Unity 8
 All Classes Functions
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 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
35 
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
47 
48  signal showTapped(point position)
49 
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  }
56 
57  hideAnimation: StandardAnimation {
58  property: "height"
59  duration: 350
60  to: panelHeight
61  easing.type: Easing.OutCubic
62  }
63 
64  onOpenedHeightChanged: {
65  if (showAnimation.running) {
66  showAnimation.restart();
67  } else if (indicators.shown) {
68  height = openedHeight;
69  }
70  }
71 
72  height: panelHeight
73  onHeightChanged: updateRevealProgressState(indicators.height - panelHeight - showHintBottomMargin, true)
74 
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 
88  if (enableRelease && revealProgress === 0) {
89  menuContent.releaseContent();
90  }
91  }
92 
93  function calculateCurrentItem(xValue, useBuffer) {
94  var rowCoordinates;
95  var itemCoordinates;
96  var currentItem;
97  var distanceFromRightEdge;
98  var bufferExceeded = false;
99 
100  if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
101 
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);
110 
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  }
118 
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;
125 
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;
133 
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  }
153 
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  }
165 
166  // eater
167  MouseArea {
168  anchors {
169  top: parent.top
170  bottom: handle.bottom
171  left: parent.left
172  right: parent.right
173  }
174  }
175 
176  VisibleIndicators {
177  id: visibleIndicators
178  }
179 
180  MenuContent {
181  id: menuContent
182  objectName: "menuContent"
183 
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
195 
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  }
211 
212  Rectangle {
213  id: handle
214 
215  color: menuContent.backgroundColor
216 
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
225 
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  }
240 
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
252 
253  EdgeDragArea {
254  id: rowDragArea
255  anchors.fill: indicatorRow
256  direction: Direction.Downwards
257  maxSilenceTime: 2000
258  distanceThreshold: 0
259 
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  }
271 
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  }
281 
282  Connections {
283  target: showAnimation
284  onRunningChanged: {
285  if (showAnimation.running) {
286  indicators.state = "commit";
287  indicatorRow.currentItemOffset = 0;
288  }
289  }
290  }
291 
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  }
302 
303  QtObject {
304  id: d
305  property bool enableIndexChangeSignal: true
306  property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
307  }
308 
309  Connections {
310  target: menuContent
311  onCurrentMenuIndexChanged: {
312  var oldActive = d.enableIndexChangeSignal;
313  if (!oldActive) return;
314  d.enableIndexChangeSignal = false;
315 
316  indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
317 
318  d.enableIndexChangeSignal = oldActive;
319  }
320  }
321 
322  Connections {
323  target: indicatorRow
324  onCurrentItemIndexChanged: {
325  var oldActive = d.enableIndexChangeSignal;
326  if (!oldActive) return;
327  d.enableIndexChangeSignal = false;
328 
329  menuContent.setCurrentMenuIndex(indicatorRow.currentItemIndex, fullyOpened || partiallyOpened);
330 
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  }
344 
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
360 
361  onStatusChanged: {
362  if (status === DirectionalDragArea.Recognized) {
363  menuContent.activateContent();
364  }
365  }
366 
367  onTapped: showTapped(Qt.point(touchSceneX, touchSceneY));
368  }
369 
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  }
381 
382  AxisVelocityCalculator {
383  id: yVelocityCalculator
384  }
385 
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"
414 
415  transitions: [
416  Transition {
417  NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
418  }
419  ]
420 
421  Component.onCompleted: initialise();
422  function initialise() {
423  visibleIndicators.load(profile);
424  indicatorRow.setDefaultItem();
425  }
426 }