on fbd182d help sign in
unity8/qml/Panel/Indicators.qml
422 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 contentEnabled: true
41 property bool initalizeItem: true
42 readonly property alias content: menuContent
43 property real unitProgress: (height - panelHeight) / (openedHeight - panelHeight)
44 property bool enableHint: true
45 property real showHintBottomMargin: 0
47 signal showTapped(point position)
49 // TODO: Perhaps we need a animation standard for showing/hiding? Each showable seems to
50 // use its own values. Need to ask design about this.
51 showAnimation: StandardAnimation {
52 property: "height"
53 to: openedHeight
54 }
56 hideAnimation: StandardAnimation {
57 property: "height"
58 duration: 350
59 to: panelHeight
60 easing.type: Easing.OutCubic
61 }
63 onOpenedHeightChanged: {
64 if (showAnimation.running) {
65 showAnimation.restart();
66 } else if (indicators.shown) {
67 height = openedHeight;
68 }
69 }
71 height: panelHeight
72 onHeightChanged: updateRevealProgressState(indicators.height - panelHeight - showHintBottomMargin, true)
74 function updateRevealProgressState(revealProgress, enableRelease) {
75 if (!showAnimation.running && !hideAnimation.running) {
76 if (revealProgress === 0) {
77 indicators.state = "initial";
78 } else if (enableHint && revealProgress > 0 && revealProgress <= hintValue) {
79 indicators.state = "hint";
80 } else if ((!enableHint || revealProgress > hintValue) && revealProgress < lockThreshold) {
81 indicators.state = "reveal";
82 } else if (revealProgress >= lockThreshold && lockThreshold > 0) {
83 indicators.state = "locked";
84 }
85 }
87 if (enableRelease && revealProgress === 0) {
88 menuContent.releaseContent();
89 }
90 }
92 function calculateCurrentItem(xValue, useBuffer) {
93 var rowCoordinates;
94 var itemCoordinates;
95 var currentItem;
96 var distanceFromRightEdge;
97 var bufferExceeded = false;
99 if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
101 /*
102 If user drags the indicator handle bar down a distance hintValue or less, this is 0.
103 If bar is dragged down a distance greater than or equal to lockThreshold, this is 1.
104 Otherwise it contains the bar's location as a fraction of the distance between hintValue (is 0) and lockThreshold (is 1).
105 */
106 var verticalProgress =
107 MathUtils.clamp((indicators.height - handle.height - hintValue) /
108 (lockThreshold - hintValue), 0, 1);
110 /*
111 Vertical velocity check. Don't change the indicator if we're moving too quickly.
112 */
113 var verticalSpeed = Math.abs(yVelocityCalculator.calculate());
114 if (verticalSpeed >= 0.05 && !initalizeItem) {
115 return;
116 }
118 /*
119 Percentage of an indicator icon's width the user's press can stray horizontally from the
120 focused icon before we change focus to another icon. E.g. a value of 0.5 means you must
121 go right a distance of half an icon's width before focus moves to the icon on the right
122 */
123 var maxBufferThreshold = 0.5;
125 /*
126 To help users find the indicator of their choice while opening the indicators, we add logic to add a
127 left/right buffer to each icon so it is harder for the focus to be moved accidentally to another icon,
128 as the user moves their finger down, but yet allows them to switch indicator if they want.
129 This buffer is wider the further the user's finger is from the top of the screen.
130 */
131 var effectiveBufferThreshold = maxBufferThreshold * verticalProgress;
133 rowCoordinates = indicatorRow.mapToItem(indicatorRow.row, xValue, 0);
134 // get the current delegate
135 currentItem = indicatorRow.row.itemAt(rowCoordinates.x, 0);
136 if (currentItem) {
137 itemCoordinates = indicatorRow.row.mapToItem(currentItem, rowCoordinates.x, 0);
138 distanceFromRightEdge = (currentItem.width - itemCoordinates.x) / (currentItem.width);
139 if (currentItem != indicatorRow.currentItem) {
140 if (Math.abs(currentItem.ownIndex - indicatorRow.currentItemIndex) > 1) {
141 bufferExceeded = true;
142 } else {
143 if (indicatorRow.currentItemIndex < currentItem.ownIndex && distanceFromRightEdge < (1 - effectiveBufferThreshold)) {
144 bufferExceeded = true;
145 } else if (indicatorRow.currentItemIndex > currentItem.ownIndex && distanceFromRightEdge > effectiveBufferThreshold) {
146 bufferExceeded = true;
147 }
148 }
149 if ((!useBuffer || (useBuffer && bufferExceeded)) || indicatorRow.currentItemIndex < 0 || indicatorRow.currentItem == null) {
150 indicatorRow.setCurrentItem(currentItem);
151 }
153 // need to re-init the distanceFromRightEdge for offset calculation
154 itemCoordinates = indicatorRow.row.mapToItem(indicatorRow.currentItem, rowCoordinates.x, 0);
155 distanceFromRightEdge = (indicatorRow.currentItem.width - itemCoordinates.x) / (indicatorRow.currentItem.width);
156 }
157 indicatorRow.currentItemOffset = 1 - (distanceFromRightEdge * 2);
158 } else if (initalizeItem) {
159 indicatorRow.setDefaultItem();
160 indicatorRow.currentItemOffset = 0;
161 }
162 initalizeItem = indicatorRow.currentItem == null;
163 }
165 // eater
166 MouseArea {
167 anchors {
168 top: parent.top
169 bottom: handle.bottom
170 left: parent.left
171 right: parent.right
172 }
173 }
175 VisibleIndicators {
176 id: visibleIndicators
177 }
179 MenuContent {
180 id: menuContent
181 objectName: "menuContent"
183 anchors {
184 left: parent.left
185 right: parent.right
186 top: indicatorRow.bottom
187 bottom: handle.top
188 }
189 indicatorsModel: visibleIndicators.model
190 clip: !indicators.fullyOpened
191 activeHeader: indicators.state == "hint" || indicators.state == "reveal"
192 enabled: contentEnabled
194 //small shadow gradient at bottom of menu
195 Rectangle {
196 anchors {
197 left: parent.left
198 right: parent.right
199 bottom: parent.bottom
200 }
201 height: units.gu(0.5)
202 gradient: Gradient {
203 GradientStop { position: 0.0; color: "transparent" }
204 GradientStop { position: 1.0; color: "black" }
205 }
206 opacity: 0.4
207 }
208 }
210 Rectangle {
211 id: handle
213 color: menuContent.backgroundColor
215 anchors {
216 left: parent.left
217 right: parent.right
218 bottom: parent.bottom
219 }
220 height: Math.max(Math.min(handleImage.height, indicators.height - handleImage.height), 0)
221 clip: height < handleImage.height
223 BorderImage {
224 id: handleImage
225 source: "graphics/handle.sci"
226 height: panelHeight
227 anchors {
228 left: parent.left
229 right: parent.right
230 bottom: parent.bottom
231 }
232 }
233 MouseArea { //prevent clicks passing through
234 anchors.fill: parent
235 }
236 }
238 IndicatorRow {
239 id: indicatorRow
240 objectName: "indicatorRow"
241 anchors {
242 left: parent.left
243 right: parent.right
244 }
245 height: indicators.panelHeight
246 indicatorsModel: visibleIndicators.model
247 state: indicators.state
248 unitProgress: indicators.unitProgress
250 EdgeDragArea {
251 id: rowDragArea
252 anchors.fill: indicatorRow
253 direction: Direction.Downwards
254 maxSilenceTime: 2000
255 distanceThreshold: 0
257 enabled: fullyOpened
258 onDraggingChanged: {
259 if (dragging) {
260 initalizeItem = true;
261 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
262 indicators.calculateCurrentItem(touchX, false);
263 } else {
264 indicators.state = "commit";
265 }
266 }
268 onTouchXChanged: {
269 indicators.calculateCurrentItem(touchX, true);
270 }
271 onTouchSceneYChanged: {
272 updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
273 yVelocityCalculator.trackedPosition = touchSceneY;
274 }
275 }
276 }
278 Connections {
279 target: showAnimation
280 onRunningChanged: {
281 if (showAnimation.running) {
282 indicators.state = "commit";
283 indicatorRow.currentItemOffset = 0;
284 }
285 }
286 }
288 Connections {
289 target: hideAnimation
290 onRunningChanged: {
291 if (hideAnimation.running) {
292 indicators.state = "initial";
293 initalizeItem = true;
294 indicatorRow.currentItemOffset = 0;
295 }
296 }
297 }
299 QtObject {
300 id: d
301 property bool enableIndexChangeSignal: true
302 property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
303 }
305 Connections {
306 target: menuContent
307 onCurrentMenuIndexChanged: {
308 var oldActive = d.enableIndexChangeSignal;
309 if (!oldActive) return;
310 d.enableIndexChangeSignal = false;
312 indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
314 d.enableIndexChangeSignal = oldActive;
315 }
316 }
318 Connections {
319 target: indicatorRow
320 onCurrentItemIndexChanged: {
321 var oldActive = d.enableIndexChangeSignal;
322 if (!oldActive) return;
323 d.enableIndexChangeSignal = false;
325 menuContent.setCurrentMenuIndex(indicatorRow.currentItemIndex, fullyOpened || partiallyOpened);
327 d.enableIndexChangeSignal = oldActive;
328 }
329 }
330 // connections to the active drag handle
331 Connections {
332 target: d.activeDragHandle
333 onTouchXChanged: {
334 indicators.calculateCurrentItem(d.activeDragHandle.touchX, true);
335 }
336 onTouchSceneYChanged: {
337 yVelocityCalculator.trackedPosition = d.activeDragHandle.touchSceneY;
338 }
339 }
341 DragHandle {
342 id: showDragHandle
343 anchors.bottom: parent.bottom
344 // go beyond parent so that it stays reachable, at the top of the screen.
345 anchors.bottomMargin: showHintBottomMargin
346 anchors.left: parent.left
347 anchors.right: parent.right
348 height: panelHeight
349 direction: Direction.Downwards
350 enabled: !indicators.shown && indicators.available
351 hintDisplacement: enableHint ? indicators.hintValue : 0
352 autoCompleteDragThreshold: maxTotalDragDistance / 2
353 stretch: true
354 maxTotalDragDistance: openedHeight - panelHeight
355 distanceThreshold: panelHeight
357 onStatusChanged: {
358 if (status === DirectionalDragArea.Recognized) {
359 menuContent.activateContent();
360 }
361 }
363 onTapped: showTapped(Qt.point(touchSceneX, touchSceneY));
364 }
366 DragHandle {
367 id: hideDragHandle
368 anchors.fill: handle
369 direction: Direction.Upwards
370 enabled: indicators.shown && indicators.available
371 hintDisplacement: indicators.hintValue
372 autoCompleteDragThreshold: maxTotalDragDistance / 6
373 stretch: true
374 maxTotalDragDistance: openedHeight - panelHeight
375 distanceThreshold: 0
376 }
378 AxisVelocityCalculator {
379 id: yVelocityCalculator
380 }
382 states: [
383 State {
384 name: "initial"
385 },
386 State {
387 name: "hint"
388 StateChangeScript {
389 script: {
390 if (d.activeDragHandle) {
391 calculateCurrentItem(d.activeDragHandle.touchX, false);
392 }
393 }
394 }
395 },
396 State {
397 name: "reveal"
398 extend: "hint"
399 },
400 State {
401 name: "locked"
402 extend: "hint"
403 },
404 State {
405 name: "commit"
406 extend: "hint"
407 }
408 ]
409 state: "initial"
411 transitions: [
412 Transition {
413 NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
414 }
415 ]
417 Component.onCompleted: initialise();
418 function initialise() {
419 visibleIndicators.load(profile);
420 indicatorRow.setDefaultItem();
421 }