on b60e2fc help sign in
unity8/qml/Panel/Indicators.qml
415 lines 14 kB history raw
1/*
2 * Copyright (C) 2013 Canonical, Ltd.
3 *
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.
7 *
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.
12 *
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/>.
15 */
17import QtQuick 2.0
18import Ubuntu.Components 0.1
19import Ubuntu.Gestures 0.1
20import Unity.Indicators 0.1 as Indicators
22import "../Components"
23import "../Components/ListItems"
24import "Indicators"
26Showable {
27 id: indicators
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 {
53 property: "height"
54 to: openedHeight
55 }
57 hideAnimation: StandardAnimation {
58 property: "height"
59 duration: 350
60 to: panelHeight
61 easing.type: Easing.OutCubic
62 }
64 onOpenedHeightChanged: {
65 if (showAnimation.running) {
66 showAnimation.restart();
67 } else if (indicators.shown) {
68 height = openedHeight;
69 }
70 }
72 height: panelHeight
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";
85 }
86 }
87 }
89 function calculateCurrentItem(xValue, useBuffer) {
90 var rowCoordinates;
91 var itemCoordinates;
92 var currentItem;
93 var distanceFromRightEdge;
94 var bufferExceeded = false;
96 if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
98 /*
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).
102 */
103 var verticalProgress =
104 MathUtils.clamp((indicators.height - handle.height - hintValue) /
105 (lockThreshold - hintValue), 0, 1);
107 /*
108 Vertical velocity check. Don't change the indicator if we're moving too quickly.
109 */
110 var verticalSpeed = Math.abs(yVelocityCalculator.calculate());
111 if (verticalSpeed >= 0.05 && !initalizeItem) {
112 return;
113 }
115 /*
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
119 */
120 var maxBufferThreshold = 0.5;
122 /*
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.
127 */
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);
133 if (currentItem) {
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;
139 } else {
140 if (indicatorRow.currentItemIndex < currentItem.ownIndex && distanceFromRightEdge < (1 - effectiveBufferThreshold)) {
141 bufferExceeded = true;
142 } else if (indicatorRow.currentItemIndex > currentItem.ownIndex && distanceFromRightEdge > effectiveBufferThreshold) {
143 bufferExceeded = true;
144 }
145 }
146 if ((!useBuffer || (useBuffer && bufferExceeded)) || indicatorRow.currentItemIndex < 0 || indicatorRow.currentItem == null) {
147 indicatorRow.setCurrentItem(currentItem);
148 }
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);
153 }
154 indicatorRow.currentItemOffset = 1 - (distanceFromRightEdge * 2);
155 } else if (initalizeItem) {
156 indicatorRow.setDefaultItem();
157 indicatorRow.currentItemOffset = 0;
158 }
159 initalizeItem = indicatorRow.currentItem == null;
160 }
162 // eater
163 MouseArea {
164 anchors {
165 top: parent.top
166 bottom: handle.bottom
167 left: parent.left
168 right: parent.right
169 }
170 }
172 VisibleIndicators {
173 id: visibleIndicators
174 }
176 MenuContent {
177 id: menuContent
178 objectName: "menuContent"
180 anchors {
181 left: parent.left
182 right: parent.right
183 top: indicatorRow.bottom
184 bottom: handle.top
185 }
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
192 Rectangle {
193 anchors {
194 left: parent.left
195 right: parent.right
196 bottom: parent.bottom
197 }
198 height: units.gu(0.5)
199 gradient: Gradient {
200 GradientStop { position: 0.0; color: "transparent" }
201 GradientStop { position: 1.0; color: "black" }
202 }
203 opacity: 0.4
204 }
205 }
207 Rectangle {
208 id: handle
210 color: menuContent.color
212 anchors {
213 left: parent.left
214 right: parent.right
215 bottom: parent.bottom
216 }
217 height: Math.max(Math.min(handleImage.height, indicators.height - handleImage.height), 0)
218 clip: height < handleImage.height
219 visible: menuContent.visible
221 BorderImage {
222 id: handleImage
223 source: "graphics/handle.sci"
224 height: panelHeight
225 anchors {
226 left: parent.left
227 right: parent.right
228 bottom: parent.bottom
229 }
230 }
231 MouseArea { //prevent clicks passing through
232 anchors.fill: parent
233 }
234 }
236 IndicatorRow {
237 id: indicatorRow
238 objectName: "indicatorRow"
239 anchors {
240 left: parent.left
241 right: parent.right
242 }
243 height: indicators.panelHeight
244 indicatorsModel: visibleIndicators.model
245 state: indicators.state
246 unitProgress: indicators.unitProgress
248 EdgeDragArea {
249 id: rowDragArea
250 anchors.fill: indicatorRow
251 direction: Direction.Downwards
252 maxSilenceTime: 2000
253 distanceThreshold: 0
255 enabled: fullyOpened
256 onDraggingChanged: {
257 if (dragging) {
258 initalizeItem = true;
259 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
260 indicators.calculateCurrentItem(touchX, false);
261 } else {
262 indicators.state = "commit";
263 indicatorRow.currentItemOffset = 0;
264 }
265 }
267 onTouchXChanged: {
268 indicators.calculateCurrentItem(touchX, true);
269 }
270 onTouchSceneYChanged: {
271 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
272 yVelocityCalculator.trackedPosition = touchSceneY;
273 }
274 }
275 }
277 Connections {
278 target: showAnimation
279 onRunningChanged: {
280 if (showAnimation.running) {
281 indicators.state = "commit";
282 indicatorRow.currentItemOffset = 0;
283 }
284 }
285 }
287 Connections {
288 target: hideAnimation
289 onRunningChanged: {
290 if (hideAnimation.running) {
291 indicators.state = "initial";
292 initalizeItem = true;
293 indicatorRow.currentItemOffset = 0;
294 }
295 }
296 }
298 QtObject {
299 id: d
300 property bool enableIndexChangeSignal: true
301 property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
302 }
304 Connections {
305 target: menuContent
306 onCurrentMenuIndexChanged: {
307 var oldActive = d.enableIndexChangeSignal;
308 if (!oldActive) return;
309 d.enableIndexChangeSignal = false;
311 indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
313 d.enableIndexChangeSignal = oldActive;
314 }
315 }
317 Connections {
318 target: indicatorRow
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;
327 }
328 }
329 // connections to the active drag handle
330 Connections {
331 target: d.activeDragHandle
332 onTouchXChanged: {
333 indicators.calculateCurrentItem(d.activeDragHandle.touchX, true);
334 }
335 onTouchSceneYChanged: {
336 yVelocityCalculator.trackedPosition = d.activeDragHandle.touchSceneY;
337 }
338 }
340 DragHandle {
341 id: showDragHandle
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
347 height: panelHeight
348 direction: Direction.Downwards
349 enabled: !indicators.shown && indicators.available
350 hintDisplacement: enableHint ? indicators.hintValue : 0
351 autoCompleteDragThreshold: maxTotalDragDistance / 2
352 stretch: true
353 maxTotalDragDistance: openedHeight - panelHeight
354 distanceThreshold: panelHeight
356 onTapped: showTapped(Qt.point(touchSceneX, touchSceneY));
357 }
359 DragHandle {
360 id: hideDragHandle
361 anchors.fill: handle
362 direction: Direction.Upwards
363 enabled: indicators.shown && indicators.available
364 hintDisplacement: indicators.hintValue
365 autoCompleteDragThreshold: maxTotalDragDistance / 6
366 stretch: true
367 maxTotalDragDistance: openedHeight - panelHeight
368 distanceThreshold: 0
369 }
371 AxisVelocityCalculator {
372 id: yVelocityCalculator
373 }
375 states: [
376 State {
377 name: "initial"
378 },
379 State {
380 name: "hint"
381 StateChangeScript {
382 script: {
383 if (d.activeDragHandle) {
384 calculateCurrentItem(d.activeDragHandle.touchX, false);
385 }
386 }
387 }
388 },
389 State {
390 name: "reveal"
391 extend: "hint"
392 },
393 State {
394 name: "locked"
395 extend: "hint"
396 },
397 State {
398 name: "commit"
399 extend: "hint"
400 }
401 ]
402 state: "initial"
404 transitions: [
405 Transition {
406 NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
407 }
408 ]
410 Component.onCompleted: initialise();
411 function initialise() {
412 visibleIndicators.load(profile);
413 indicatorRow.setDefaultItem();
414 }