1 /****************************************************************************
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
7 ** This file is part of the Qt Components project.
9 ** $QT_BEGIN_LICENSE:BSD$
10 ** You may use this file under the terms of the BSD license as follows:
12 ** "Redistribution and use in source and binary forms, with or without
13 ** modification, are permitted provided that the following conditions are
15 ** * Redistributions of source code must retain the above copyright
16 ** notice, this list of conditions and the following disclaimer.
17 ** * Redistributions in binary form must reproduce the above copyright
18 ** notice, this list of conditions and the following disclaimer in
19 ** the documentation and/or other materials provided with the
21 ** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
22 ** the names of its contributors may be used to endorse or promote
23 ** products derived from this software without specific prior written
26 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
39 ****************************************************************************/
42 import "SectionScroller.js" as Sections
48 property Flickable flickableItem: null
49 property int orientation: Qt.Vertical
50 property bool interactive: true
51 property int policy: Android.ScrollBarWhenScrolling
52 property bool privateSectionScroller: false
53 property bool platformInverted: false
55 //implicit values for qml designer when no Flickable is present
56 implicitHeight: privateStyle.scrollBarThickness * (orientation == Qt.Vertical ? 3 : 1)
57 implicitWidth: privateStyle.scrollBarThickness * (orientation == Qt.Horizontal ? 3 : 1)
59 if (flickableItem && orientation == Qt.Vertical)
60 return Math.floor(Math.round(flickableItem.height) - anchors.topMargin - anchors.bottomMargin)
64 if (flickableItem && orientation == Qt.Horizontal)
65 return Math.floor(Math.round(flickableItem.width) - anchors.leftMargin - anchors.rightMargin)
68 opacity: internal.rootOpacity
70 onFlickableItemChanged: internal.initSectionScroller();
71 onPrivateSectionScrollerChanged: internal.initSectionScroller();
73 //For showing explicitly a ScrollBar if policy is Android.ScrollBarWhenScrolling
74 function flash(type) {
75 if (policy == Android.ScrollBarWhenScrolling && internal.scrollBarNeeded) {
76 flashEffect.type = (type == undefined) ? Android.FadeOut : type
83 onCurrentOrientationChanged: flash()
87 target: root.privateSectionScroller ? flickableItem : null
88 onModelChanged: internal.initSectionScroller()
92 id: listItemInteraction
93 sourceComponent: flickableItem && flickableItem.hasOwnProperty("currentIndex") && flickableItem.activeFocus ? listItemInteractionComponent : undefined
97 id: listItemInteractionComponent
101 onListInteractionModeChanged: flash()
102 onPrivateListItemKeyNavigation: flash()
109 property int hideTimeout: 500
110 property int pageStepY: flickableItem ? Math.floor(flickableItem.visibleArea.heightRatio * flickableItem.contentHeight) : NaN
111 property int pageStepX: flickableItem ? Math.floor(flickableItem.visibleArea.widthRatio * flickableItem.contentWidth) : NaN
112 property int handleY: flickableItem ? Math.floor(handle.y / flickableItem.height * flickableItem.contentHeight) : NaN
113 property int handleX: flickableItem ? Math.floor(handle.x / flickableItem.width * flickableItem.contentWidth) : NaN
114 property int maximumY: flickableItem ? Math.floor(Math.min(flickableItem.contentHeight - root.height, flickableItem.contentY)) : NaN
115 property int maximumX: flickableItem ? Math.floor(Math.min(flickableItem.contentWidth - root.width, flickableItem.contentX)) : NaN
116 property bool scrollBarNeeded: root.visible && hasScrollableContent()
117 //Sets currentSection to empty string when flickableItem.currentSection is null
118 property string currentSection: flickableItem ? flickableItem.currentSection || "" : ""
119 //To be able to pressed on trackMouseArea, opacity needs to be greater than 0
120 property real rootOpacity: root.privateSectionScroller ? 0.01 : 0
122 // The following properties are used for calculating handle position and size:
123 // Minimum allowed handle length
124 property real minHandleLength: 3 * Math.floor(privateStyle.scrollBarThickness)
125 // Scroll bar "container" length
126 property real totalLength: !flickableItem ? NaN : (orientation == Qt.Vertical ? root.height : root.width)
127 property real relativeHandlePosition: !flickableItem ? NaN : (orientation == Qt.Vertical
128 ? flickableItem.visibleArea.yPosition
129 : flickableItem.visibleArea.xPosition)
130 property real lengthRatio: !flickableItem ? NaN : (orientation == Qt.Vertical
131 ? flickableItem.visibleArea.heightRatio
132 : flickableItem.visibleArea.widthRatio)
133 // Scroll bar handle length under normal circumstances
134 property real staticHandleLength: Math.max(minHandleLength, lengthRatio * totalLength)
135 // Adjustment factor needed to calculate correct position, which is needed because
136 // relativeHandlePosition and lengthRatio are assuming default handle length. Dividend is
137 // the maximum handle position taking into account restricted minimum handle length, whereas
138 // divisor is the maximum handle position if default handle size would be used.
139 property real handlePositionAdjustment: (totalLength - staticHandleLength) / (totalLength * (1 - lengthRatio))
140 property real handlePosition
142 // handle's position, which is adjusted to take into account non-default handle
143 // length and restricted never to exceed flickable boundaries
144 var pos = totalLength * relativeHandlePosition * handlePositionAdjustment
145 return Math.min(Math.max(0, pos), totalLength - minHandleLength)
147 property real dynamicHandleLength
148 dynamicHandleLength: {
149 // dynamic handle length, which may differ from static due to bounds behavior, i.e.
150 // handle gets shorter when content exceeds flickable boundaries
152 if (relativeHandlePosition >= 1 - lengthRatio) // overflow
153 length = 1 - relativeHandlePosition
154 else if (relativeHandlePosition < 0) // underflow
155 length = lengthRatio + relativeHandlePosition
158 return Math.max(minHandleLength, length * totalLength)
161 // Show flash effect in case if no flicking (flickableItem.moving) has occured
162 // but there is a need to indicate scrollable content.
163 onScrollBarNeededChanged: {
164 if (stateGroup.state == "") {
173 * Checks whether ScrollBar is needed or not
174 * based on Flickable visibleArea height and width ratios
176 function hasScrollableContent() {
179 var ratio = orientation == Qt.Vertical ? flickableItem.visibleArea.heightRatio : flickableItem.visibleArea.widthRatio
180 return ratio < 1.0 && ratio > 0
183 * Does Page by Page movement of flickableItem
184 * when ScrollBar Track is being clicked/pressed
186 * @see #moveToLongTapPosition
188 function doPageStep() {
189 if (orientation == Qt.Vertical) {
190 if (trackMouseArea.mouseY > (handle.height / 2 + handle.y)) {
191 flickableItem.contentY += pageStepY
192 flickableItem.contentY = maximumY
194 else if (trackMouseArea.mouseY < (handle.height / 2 + handle.y)) {
195 flickableItem.contentY -= pageStepY
196 flickableItem.contentY = Math.max(0, flickableItem.contentY)
199 if (trackMouseArea.mouseX > (handle.width / 2 + handle.x)) {
200 flickableItem.contentX += pageStepX
201 flickableItem.contentX = maximumX
203 else if (trackMouseArea.mouseX < (handle.width / 2 + handle.x)) {
204 flickableItem.contentX -= pageStepX
205 flickableItem.contentX = Math.max(0, flickableItem.contentX)
210 * Does movement of flickableItem
211 * when ScrollBar Handle is being dragged
213 function moveToHandlePosition() {
214 if (orientation == Qt.Vertical)
215 flickableItem.contentY = handleY
217 flickableItem.contentX = handleX
220 * Moves flickableItem's content according to given mouseArea movement
221 * when mouseArea is pressed long
222 * Tries to position the handle and content in center of mouse position enough
226 function moveToLongTapPosition(mouseArea) {
227 if (orientation == Qt.Vertical) {
228 if (Math.abs(mouseArea.mouseY - (handle.height / 2 + handle.y)) < privateStyle.scrollBarThickness)
229 return //if change is not remarkable enough, do nothing otherwise it would cause annoying flickering effect
230 if (mouseArea.mouseY > (handle.height / 2 + handle.y)) {
231 flickableItem.contentY += Math.floor(privateStyle.scrollBarThickness)
232 flickableItem.contentY = maximumY
234 else if (mouseArea.mouseY < (handle.height / 2 + handle.y)) {
235 flickableItem.contentY -= Math.floor(privateStyle.scrollBarThickness)
236 flickableItem.contentY = Math.floor(Math.max(0, flickableItem.contentY))
239 if (Math.abs(mouseArea.mouseX - (handle.width / 2 + handle.x)) < privateStyle.scrollBarThickness)
240 return //if change is not remarkable enough, do nothing otherwise it would cause annoying flickering effect
241 if (mouseArea.mouseX > (handle.width / 2 + handle.x)) {
242 flickableItem.contentX += Math.floor(privateStyle.scrollBarThickness)
243 flickableItem.contentX = maximumX
245 else if (mouseArea.mouseX < (handle.width / 2 + handle.x)) {
246 flickableItem.contentX -= Math.floor(privateStyle.scrollBarThickness)
247 flickableItem.contentX = Math.floor(Math.max(0, flickableItem.contentX))
252 function adjustContentPosition(y) {
253 if (y < 0 || y > trackMouseArea.height)
256 var sect = Sections.closestSection(y / trackMouseArea.height);
257 var idx = Sections.indexOf(sect);
258 currentSection = sect;
259 flickableItem.positionViewAtIndex(idx, ListView.Beginning);
262 function initSectionScroller() {
263 if (root.privateSectionScroller && flickableItem && flickableItem.model)
264 Sections.initSectionData(flickableItem);
271 source: privateStyle.imagePath((orientation == Qt.Vertical
272 ? "qtg_fr_scrollbar_v_track_normal"
273 : "qtg_fr_scrollbar_h_track_normal"),
274 root.platformInverted)
277 border.right: orientation == Qt.Horizontal ? 7 : 0
278 border.left: orientation == Qt.Horizontal ? 7 : 0
279 border.top: orientation == Qt.Vertical ? 7 : 0
280 border.bottom: orientation == Qt.Vertical ? 7 : 0
281 onVisibleChanged: { idleEffect.complete(); flashEffect.complete() }
285 id: sectionScrollBackground
286 anchors.right: trackMouseArea.right
287 width: flickableItem ? flickableItem.width : 0
288 height: platformStyle.fontSizeMedium * 5
289 sourceComponent: root.privateSectionScroller ? sectionScrollComponent : null
293 id: sectionScrollComponent
295 id: indexFeedbackBackground
296 objectName: "indexFeedbackBackground"
297 source: privateStyle.imagePath("qtg_fr_popup_transparent", root.platformInverted)
298 border { left: platformStyle.borderSizeMedium; top: platformStyle.borderSizeMedium; right: platformStyle.borderSizeMedium; bottom: platformStyle.borderSizeMedium }
299 visible: trackMouseArea.pressed
302 id: indexFeedbackText
303 objectName: "indexFeedbackText"
304 color: root.platformInverted ? platformStyle.colorNormalDarkInverted // intentionally dark inverted
305 : platformStyle.colorNormalLight
308 leftMargin: platformStyle.paddingLarge;
310 rightMargin: platformStyle.paddingLarge;
311 verticalCenter: parent.verticalCenter
314 family: platformStyle.fontFamilyRegular;
315 pixelSize: indexFeedbackText.text.length == 1 ? platformStyle.fontSizeMedium * 4 : platformStyle.fontSizeMedium * 2;
316 capitalization: indexFeedbackText.text.length == 1 ? Font.AllUppercase : Font.MixedCase
318 text: internal.currentSection
319 horizontalAlignment: Text.AlignLeft
320 elide: Text.ElideRight
324 when: (handle.y + (handle.height / 2)) - (sectionScrollBackground.height / 2) < 0
326 target: sectionScrollBackground
327 anchors { verticalCenter: undefined; top: track.top; bottom: undefined }
331 when: (handle.y + (handle.height / 2)) + (sectionScrollBackground.height / 2) >= track.height
333 target: sectionScrollBackground
334 anchors { verticalCenter: undefined; top: undefined; bottom: track.bottom }
338 when: (handle.y + (handle.height / 2)) - (sectionScrollBackground.height / 2) >= 0
340 target: sectionScrollBackground
341 anchors { verticalCenter: handle.verticalCenter; top: undefined; bottom: undefined }
348 // MouseArea for the move content "page by page" by tapping and scroll to press-and-hold position
351 objectName: "trackMouseArea"
352 property bool longPressed: false
353 enabled: root.privateSectionScroller || interactive
355 top: root.privateSectionScroller ? parent.top : undefined;
356 bottom: root.privateSectionScroller ? parent.bottom : undefined;
357 right: root.privateSectionScroller ? parent.right : undefined;
358 fill: root.privateSectionScroller ? undefined : (flickableItem ? track : undefined)
360 width: root.privateSectionScroller ? privateStyle.scrollBarThickness * 3 : undefined
362 target: root.privateSectionScroller ? sectionScrollBackground : undefined
363 // axis is set XandY to prevent flickable from stealing the mouse event
364 // SectionScroller is anchored to the right side of the mouse area so the user
365 // won't be able to drag it along the X axis
366 axis: root.privateSectionScroller ? Drag.XandYAxis : 0
367 minimumY: root.privateSectionScroller ? (flickableItem ? flickableItem.y : 0) : 0
368 maximumY: root.privateSectionScroller ? (flickableItem ? (trackMouseArea.height - sectionScrollBackground.height) : 0) : 0
372 if (!root.privateSectionScroller)
376 onReleased: longPressed = false
379 if (root.privateSectionScroller)
380 internal.adjustContentPosition(trackMouseArea.mouseY);
384 if (root.privateSectionScroller && trackMouseArea.pressed)
385 internal.adjustContentPosition(trackMouseArea.mouseY);
389 id: pressAndHoldTimer
390 running: trackMouseArea.longPressed
393 onTriggered: { internal.moveToLongTapPosition(trackMouseArea); privateStyle.play(Android.SensitiveSlider) }
399 source: privateStyle.imagePath(handleFileName(), root.platformInverted)
400 x: orientation == Qt.Horizontal ? internal.handlePosition : NaN
401 y: orientation == Qt.Vertical ? internal.handlePosition : NaN
402 height: orientation == Qt.Vertical ? internal.dynamicHandleLength : root.height
403 width: orientation == Qt.Horizontal ? internal.dynamicHandleLength : root.width
404 border.right: orientation == Qt.Horizontal ? 7 : 0
405 border.left: orientation == Qt.Horizontal ? 7 : 0
406 border.top: orientation == Qt.Vertical ? 7 : 0
407 border.bottom: orientation == Qt.Vertical ? 7 : 0
409 function handleFileName() {
410 var fileName = (orientation == Qt.Vertical ? "qtg_fr_scrollbar_v_handle_" :
411 "qtg_fr_scrollbar_h_handle_")
413 fileName += "indicative"
414 else if (handleMouseArea.pressed)
415 fileName += "pressed"
424 objectName: "handleMouseArea"
425 property real maxDragY: flickableItem ? flickableItem.height - handle.height - root.anchors.topMargin - root.anchors.bottomMargin : NaN
426 property real maxDragX: flickableItem ? flickableItem.width - handle.width - root.anchors.leftMargin - root.anchors.rightMargin : NaN
427 enabled: interactive && !root.privateSectionScroller
428 width: orientation == Qt.Vertical ? 3 * privateStyle.scrollBarThickness : handle.width
429 height: orientation == Qt.Horizontal ? 3 * privateStyle.scrollBarThickness : handle.height
431 verticalCenter: flickableItem ? handle.verticalCenter : undefined
432 horizontalCenter: flickableItem ? handle.horizontalCenter : undefined
436 axis: orientation == Qt.Vertical ? Drag.YAxis : Drag.XAxis
442 onPositionChanged: internal.moveToHandlePosition()
459 SequentialAnimation {
463 indicateEffect.stop()
464 if (internal.scrollBarNeeded && root.policy == Android.ScrollBarWhenScrolling)
468 PauseAnimation { duration: root.interactive ? 1500 : 0 }
472 to: internal.rootOpacity
473 duration: internal.hideTimeout
476 SequentialAnimation {
478 property int type: Android.FadeOut
484 duration: (flashEffect.type == Android.FadeInFadeOut) ? internal.hideTimeout : 0
489 to: internal.rootOpacity
490 duration: internal.hideTimeout
498 when: handleMouseArea.pressed
502 when: trackMouseArea.longPressed
506 when: trackMouseArea.pressed && !trackMouseArea.longPressed && !root.privateSectionScroller
510 when: (internal.scrollBarNeeded && flickableItem.moving) ||
511 (trackMouseArea.pressed && root.privateSectionScroller)
520 ScriptAction { script: privateStyle.play(Android.BasicSlider) }
521 ScriptAction { script: indicateEffect.play() }
525 ScriptAction { script: internal.doPageStep() }
526 ScriptAction { script: privateStyle.play(Android.BasicSlider) }
527 ScriptAction { script: indicateEffect.play() }
532 ScriptAction { script: indicateEffect.play() }
537 ScriptAction { script: privateStyle.play(Android.BasicSlider) }
538 ScriptAction { script: indicateEffect.play() }
542 ScriptAction { script: indicateEffect.play() }
547 ScriptAction { script: privateStyle.play(Android.BasicSlider) }
548 ScriptAction { script: idleEffect.play() }
552 ScriptAction { script: idleEffect.play() }