Unity 8
 All Classes Functions Properties
Indicators.qml
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  */
16 
17 import QtQuick 2.0
18 import Ubuntu.Components 0.1
19 import Ubuntu.Gestures 0.1
20 import Unity.Indicators 0.1 as Indicators
21 
22 import "../Components"
23 import "../Components/ListItems"
24 import "Indicators"
25 
26 Showable {
27  id: indicators
28 
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
36 
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)
46 
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
54 
55  // Re-size if we've changed the openHeight while shown.
56  onToChanged: {
57  if (indicators.shown) {
58  indicators.show();
59  }
60  }
61  }
62 
63  hideAnimation: StandardAnimation {
64  property: "height"
65  duration: 350
66  to: panelHeight
67  easing.type: Easing.OutCubic
68  }
69 
70  height: panelHeight
71  onHeightChanged: updateRevealProgressState(indicators.height - panelHeight, true)
72 
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  }
85 
86  if (enableRelease && revealProgress === 0) {
87  menuContent.releaseContent();
88  }
89  }
90 
91  function calculateCurrentItem(xValue, useBuffer) {
92  var rowCoordinates;
93  var itemCoordinates;
94  var currentItem;
95  var distanceFromRightEdge;
96  var bufferExceeded = false;
97 
98  if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
99 
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);
108 
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  }
116 
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;
123 
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;
131 
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  }
151 
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  }
163 
164  // eater
165  MouseArea {
166  anchors {
167  top: parent.top
168  bottom: handle.bottom
169  left: parent.left
170  right: parent.right
171  }
172  }
173 
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  }
184 
185  VisibleIndicators {
186  id: visibleIndicators
187  }
188 
189  MenuContent {
190  id: menuContent
191  objectName: "menuContent"
192 
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
203 
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  }
219 
220  Rectangle {
221  id: handle
222 
223  color: menuContent.backgroundColor
224 
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
232 
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  }
247 
248  PanelBackground {
249  anchors.fill: indicatorRow
250  }
251 
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
263 
264  EdgeDragArea {
265  id: rowDragArea
266  anchors.fill: indicatorRow
267  direction: Direction.Downwards
268  maxSilenceTime: 2000
269  distanceThreshold: 0
270 
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  }
281 
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  }
291 
292  Connections {
293  target: showAnimation
294  onRunningChanged: {
295  if (showAnimation.running) {
296  indicators.state = "commit";
297  indicatorRow.currentItemOffset = 0;
298  }
299  }
300  }
301 
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  }
312 
313  QtObject {
314  id: d
315  property bool enableIndexChangeSignal: true
316  property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
317  }
318 
319  Connections {
320  target: menuContent
321  onCurrentMenuIndexChanged: {
322  var oldActive = d.enableIndexChangeSignal;
323  if (!oldActive) return;
324  d.enableIndexChangeSignal = false;
325 
326  indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
327 
328  d.enableIndexChangeSignal = oldActive;
329  }
330  }
331 
332  Connections {
333  target: indicatorRow
334  onCurrentItemIndexChanged: {
335  var oldActive = d.enableIndexChangeSignal;
336  if (!oldActive) return;
337  d.enableIndexChangeSignal = false;
338 
339  menuContent.setCurrentMenuIndex(indicatorRow.currentItemIndex, fullyOpened || partiallyOpened);
340 
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  }
354 
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)
370 
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  }
388 
389  AxisVelocityCalculator {
390  id: yVelocityCalculator
391  }
392 
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"
425 
426  transitions: [
427  Transition {
428  NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
429  }
430  ]
431 
432  Component.onCompleted: initialise();
433  function initialise() {
434  visibleIndicators.load(profile);
435  }
436 }