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 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 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
46 
47  signal showTapped(point position)
48 
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  }
55 
56  hideAnimation: StandardAnimation {
57  property: "height"
58  duration: 350
59  to: panelHeight
60  easing.type: Easing.OutCubic
61  }
62 
63  onOpenedHeightChanged: {
64  if (showAnimation.running) {
65  showAnimation.restart();
66  } else if (indicators.shown) {
67  height = openedHeight;
68  }
69  }
70 
71  height: panelHeight
72  onHeightChanged: updateRevealProgressState(indicators.height - panelHeight - showHintBottomMargin, true)
73 
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  }
86 
87  if (enableRelease && revealProgress === 0) {
88  menuContent.releaseContent();
89  }
90  }
91 
92  function calculateCurrentItem(xValue, useBuffer) {
93  var rowCoordinates;
94  var itemCoordinates;
95  var currentItem;
96  var distanceFromRightEdge;
97  var bufferExceeded = false;
98 
99  if (indicators.state == "commit" || indicators.state == "locked" || showAnimation.running || hideAnimation.running) return;
100 
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);
109 
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  }
117 
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;
124 
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;
132 
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  }
152 
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  }
164 
165  // eater
166  MouseArea {
167  anchors {
168  top: parent.top
169  bottom: handle.bottom
170  left: parent.left
171  right: parent.right
172  }
173  }
174 
175  VisibleIndicators {
176  id: visibleIndicators
177  }
178 
179  MenuContent {
180  id: menuContent
181  objectName: "menuContent"
182 
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
193 
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  }
209 
210  Rectangle {
211  id: handle
212 
213  color: menuContent.backgroundColor
214 
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
222 
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  }
237 
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
249 
250  EdgeDragArea {
251  id: rowDragArea
252  anchors.fill: indicatorRow
253  direction: Direction.Downwards
254  maxSilenceTime: 2000
255  distanceThreshold: 0
256 
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  indicatorRow.currentItemOffset = 0;
266  }
267  }
268 
269  onTouchXChanged: {
270  indicators.calculateCurrentItem(touchX, true);
271  }
272  onTouchSceneYChanged: {
273  updateRevealProgressState(Math.max(touchSceneY - panelHeight, hintValue), false);
274  yVelocityCalculator.trackedPosition = touchSceneY;
275  }
276  }
277  }
278 
279  Connections {
280  target: showAnimation
281  onRunningChanged: {
282  if (showAnimation.running) {
283  indicators.state = "commit";
284  indicatorRow.currentItemOffset = 0;
285  }
286  }
287  }
288 
289  Connections {
290  target: hideAnimation
291  onRunningChanged: {
292  if (hideAnimation.running) {
293  indicators.state = "initial";
294  initalizeItem = true;
295  indicatorRow.currentItemOffset = 0;
296  }
297  }
298  }
299 
300  QtObject {
301  id: d
302  property bool enableIndexChangeSignal: true
303  property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
304  }
305 
306  Connections {
307  target: menuContent
308  onCurrentMenuIndexChanged: {
309  var oldActive = d.enableIndexChangeSignal;
310  if (!oldActive) return;
311  d.enableIndexChangeSignal = false;
312 
313  indicatorRow.setCurrentItemIndex(menuContent.currentMenuIndex);
314 
315  d.enableIndexChangeSignal = oldActive;
316  }
317  }
318 
319  Connections {
320  target: indicatorRow
321  onCurrentItemIndexChanged: {
322  var oldActive = d.enableIndexChangeSignal;
323  if (!oldActive) return;
324  d.enableIndexChangeSignal = false;
325 
326  menuContent.setCurrentMenuIndex(indicatorRow.currentItemIndex, fullyOpened || partiallyOpened);
327 
328  d.enableIndexChangeSignal = oldActive;
329  }
330  }
331  // connections to the active drag handle
332  Connections {
333  target: d.activeDragHandle
334  onTouchXChanged: {
335  indicators.calculateCurrentItem(d.activeDragHandle.touchX, true);
336  }
337  onTouchSceneYChanged: {
338  yVelocityCalculator.trackedPosition = d.activeDragHandle.touchSceneY;
339  }
340  }
341 
342  DragHandle {
343  id: showDragHandle
344  anchors.bottom: parent.bottom
345  // go beyond parent so that it stays reachable, at the top of the screen.
346  anchors.bottomMargin: showHintBottomMargin
347  anchors.left: parent.left
348  anchors.right: parent.right
349  height: panelHeight
350  direction: Direction.Downwards
351  enabled: !indicators.shown && indicators.available
352  hintDisplacement: enableHint ? indicators.hintValue : 0
353  autoCompleteDragThreshold: maxTotalDragDistance / 2
354  stretch: true
355  maxTotalDragDistance: openedHeight - panelHeight
356  distanceThreshold: panelHeight
357 
358  onStatusChanged: {
359  if (status === DirectionalDragArea.Recognized) {
360  menuContent.activateContent();
361  }
362  }
363 
364  onTapped: showTapped(Qt.point(touchSceneX, touchSceneY));
365  }
366 
367  DragHandle {
368  id: hideDragHandle
369  anchors.fill: handle
370  direction: Direction.Upwards
371  enabled: indicators.shown && indicators.available
372  hintDisplacement: indicators.hintValue
373  autoCompleteDragThreshold: maxTotalDragDistance / 6
374  stretch: true
375  maxTotalDragDistance: openedHeight - panelHeight
376  distanceThreshold: 0
377  }
378 
379  AxisVelocityCalculator {
380  id: yVelocityCalculator
381  }
382 
383  states: [
384  State {
385  name: "initial"
386  },
387  State {
388  name: "hint"
389  StateChangeScript {
390  script: {
391  if (d.activeDragHandle) {
392  calculateCurrentItem(d.activeDragHandle.touchX, false);
393  }
394  }
395  }
396  },
397  State {
398  name: "reveal"
399  extend: "hint"
400  },
401  State {
402  name: "locked"
403  extend: "hint"
404  },
405  State {
406  name: "commit"
407  extend: "hint"
408  }
409  ]
410  state: "initial"
411 
412  transitions: [
413  Transition {
414  NumberAnimation {targets: [indicatorRow, menuContent]; property: "y"; duration: 300; easing.type: Easing.OutCubic}
415  }
416  ]
417 
418  Component.onCompleted: initialise();
419  function initialise() {
420  visibleIndicators.load(profile);
421  indicatorRow.setDefaultItem();
422  }
423 }