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