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