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