2 * Copyright (C) 2013 Canonical, Ltd.
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 import Ubuntu.Components 0.1
19 import Ubuntu.Gestures 0.1
20 import Unity.Indicators 0.1 as Indicators
22 import "../Components"
23 import "../Components/ListItems"
29 property real openedHeight: units.gu(71)
30 property int panelHeight: units.gu(3)
31 property alias overFlowWidth: indicatorRow.overFlowWidth
32 property alias showAll: indicatorRow.showAll
33 // TODO: This should be sourced by device type (eg "desktop", "tablet", "phone"...)
34 property string profile: indicatorProfile
36 readonly property real hintValue: panelHeight + menuContent.headerHeight
37 readonly property int lockThreshold: openedHeight / 2
38 property bool fullyOpened: height == openedHeight
39 property bool partiallyOpened: height > panelHeight && !fullyOpened
40 property bool fullyClosed: height <= panelHeight
41 property bool contentEnabled: true
42 property bool initalizeItem: true
43 readonly property alias content: menuContent
44 property real unitProgress: (height - panelHeight) / (openedHeight - panelHeight)
45 property bool enableHint: true
46 property real showHintBottomMargin: 0
48 signal showTapped(point position)
50 // TODO: Perhaps we need a animation standard for showing/hiding? Each showable seems to
51 // use its own values. Need to ask design about this.
52 showAnimation: StandardAnimation {
57 hideAnimation: StandardAnimation {
61 easing.type: Easing.OutCubic
64 onOpenedHeightChanged: {
65 if (showAnimation.running) {
66 showAnimation.restart();
67 } else if (indicators.shown) {
68 height = openedHeight;
73 onHeightChanged: updateRevealProgressState(indicators.height - panelHeight - showHintBottomMargin, true)
75 function updateRevealProgressState(revealProgress, enableRelease) {
76 if (!showAnimation.running && !hideAnimation.running) {
77 if (revealProgress === 0) {
78 indicators.state = "initial";
79 } else if (enableHint && revealProgress > 0 && revealProgress <= hintValue) {
80 indicators.state = "hint";
81 } else if ((!enableHint || revealProgress > hintValue) && revealProgress < lockThreshold) {
82 indicators.state = "reveal";
83 } else if (revealProgress >= lockThreshold && lockThreshold > 0) {
84 indicators.state = "locked";
89 function calculateCurrentItem(xValue, useBuffer) {
93 var distanceFromRightEdge;
94 var bufferExceeded = false;
96 if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
99 If user drags the indicator handle bar down a distance hintValue or less, this is 0.
100 If bar is dragged down a distance greater than or equal to lockThreshold, this is 1.
101 Otherwise it contains the bar's location as a fraction of the distance between hintValue (is 0) and lockThreshold (is 1).
103 var verticalProgress =
104 MathUtils.clamp((indicators.height - handle.height - hintValue) /
105 (lockThreshold - hintValue), 0, 1);
108 Vertical velocity check. Don't change the indicator if we're moving too quickly.
110 var verticalSpeed = Math.abs(yVelocityCalculator.calculate());
111 if (verticalSpeed >= 0.05 && !initalizeItem) {
116 Percentage of an indicator icon's width the user's press can stray horizontally from the
117 focused icon before we change focus to another icon. E.g. a value of 0.5 means you must
118 go right a distance of half an icon's width before focus moves to the icon on the right
120 var maxBufferThreshold = 0.5;
123 To help users find the indicator of their choice while opening the indicators, we add logic to add a
124 left/right buffer to each icon so it is harder for the focus to be moved accidentally to another icon,
125 as the user moves their finger down, but yet allows them to switch indicator if they want.
126 This buffer is wider the further the user's finger is from the top of the screen.
128 var effectiveBufferThreshold = maxBufferThreshold * verticalProgress;
130 rowCoordinates = indicatorRow.mapToItem(indicatorRow.row, xValue, 0);
131 // get the current delegate
132 currentItem = indicatorRow.row.itemAt(rowCoordinates.x, 0);
134 itemCoordinates = indicatorRow.row.mapToItem(currentItem, rowCoordinates.x, 0);
135 distanceFromRightEdge = (currentItem.width - itemCoordinates.x) / (currentItem.width);
136 if (currentItem != indicatorRow.currentItem) {
137 if (Math.abs(currentItem.ownIndex - indicatorRow.currentItemIndex) > 1) {
138 bufferExceeded = true;
140 if (indicatorRow.currentItemIndex < currentItem.ownIndex && distanceFromRightEdge < (1 - effectiveBufferThreshold)) {
141 bufferExceeded = true;
142 } else if (indicatorRow.currentItemIndex > currentItem.ownIndex && distanceFromRightEdge > effectiveBufferThreshold) {
143 bufferExceeded = true;
146 if ((!useBuffer || (useBuffer && bufferExceeded)) || indicatorRow.currentItemIndex < 0 || indicatorRow.currentItem == null) {
147 indicatorRow.setCurrentItem(currentItem);
150 // need to re-init the distanceFromRightEdge for offset calculation
151 itemCoordinates = indicatorRow.row.mapToItem(indicatorRow.currentItem, rowCoordinates.x, 0);
152 distanceFromRightEdge = (indicatorRow.currentItem.width - itemCoordinates.x) / (indicatorRow.currentItem.width);
154 indicatorRow.currentItemOffset = 1 - (distanceFromRightEdge * 2);
155 } else if (initalizeItem) {
156 indicatorRow.setDefaultItem();
157 indicatorRow.currentItemOffset = 0;
159 initalizeItem = indicatorRow.currentItem == null;
166 bottom: handle.bottom
173 id: visibleIndicators
178 objectName: "menuContent"
183 top: indicatorRow.bottom
186 indicatorsModel: visibleIndicators.model
187 visible: indicators.partiallyOpened || indicators.fullyOpened
188 clip: indicators.partiallyOpened
189 enabled: contentEnabled
191 //small shadow gradient at bottom of menu
196 bottom: parent.bottom
198 height: units.gu(0.5)
200 GradientStop { position: 0.0; color: "transparent" }
201 GradientStop { position: 1.0; color: "black" }
210 color: menuContent.color
215 bottom: parent.bottom
217 height: Math.max(Math.min(handleImage.height, indicators.height - handleImage.height), 0)
218 clip: height < handleImage.height
219 visible: menuContent.visible
223 source: "graphics/handle.sci"
228 bottom: parent.bottom
231 MouseArea { //prevent clicks passing through
238 objectName: "indicatorRow"
243 height: indicators.panelHeight
244 indicatorsModel: visibleIndicators.model
245 state: indicators.state
246 unitProgress: indicators.unitProgress
250 anchors.fill: indicatorRow
251 direction: Direction.Downwards
258 initalizeItem = true;
259 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
260 indicators.calculateCurrentItem(touchX, false);
262 indicators.state = "commit";
263 indicatorRow.currentItemOffset = 0;
268 indicators.calculateCurrentItem(touchX, true);
270 onTouchSceneYChanged: {
271 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
272 yVelocityCalculator.trackedPosition = touchSceneY;
278 target: showAnimation
280 if (showAnimation.running) {
281 indicators.state = "commit";
282 indicatorRow.currentItemOffset = 0;
288 target: hideAnimation
290 if (hideAnimation.running) {
291 indicators.state = "initial";
292 initalizeItem = true;
293 indicatorRow.currentItemOffset = 0;
300 property bool enableIndexChangeSignal: true
301 property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
306 onCurrentMenuIndexChanged: {
307 var oldActive = d.enableIndexChangeSignal;
308 if (!oldActive) return;
309 d.enableIndexChangeSignal = false;
311 indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
313 d.enableIndexChangeSignal = oldActive;
319 onCurrentItemIndexChanged: {
320 var oldActive = d.enableIndexChangeSignal;
321 if (!oldActive) return;
322 d.enableIndexChangeSignal = false;
324 menuContent.setCurrentMenuIndex(indicatorRow.currentItemIndex, fullyOpened || partiallyOpened);
326 d.enableIndexChangeSignal = oldActive;
329 // connections to the active drag handle
331 target: d.activeDragHandle
333 indicators.calculateCurrentItem(d.activeDragHandle.touchX, true);
335 onTouchSceneYChanged: {
336 yVelocityCalculator.trackedPosition = d.activeDragHandle.touchSceneY;
342 anchors.bottom: parent.bottom
343 // go beyond parent so that it stays reachable, at the top of the screen.
344 anchors.bottomMargin: showHintBottomMargin
345 anchors.left: parent.left
346 anchors.right: parent.right
348 direction: Direction.Downwards
349 enabled: !indicators.shown && indicators.available
350 hintDisplacement: enableHint ? indicators.hintValue : 0
351 autoCompleteDragThreshold: maxTotalDragDistance / 2
353 maxTotalDragDistance: openedHeight - panelHeight
354 distanceThreshold: panelHeight
356 onTapped: showTapped(Qt.point(touchSceneX, touchSceneY));
362 direction: Direction.Upwards
363 enabled: indicators.shown && indicators.available
364 hintDisplacement: indicators.hintValue
365 autoCompleteDragThreshold: maxTotalDragDistance / 6
367 maxTotalDragDistance: openedHeight - panelHeight
371 AxisVelocityCalculator {
372 id: yVelocityCalculator
383 if (d.activeDragHandle) {
384 calculateCurrentItem(d.activeDragHandle.touchX, false);
406 NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
410 Component.onCompleted: initialise();
411 function initialise() {
412 visibleIndicators.load(profile);
413 indicatorRow.setDefaultItem();