Unity 8
PanelMenu.qml
1 /*
2  * Copyright (C) 2014-2016 Canonical, Ltd.
3  * Copyright (C) 2020 UBports Foundation
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import QtQuick 2.12
19 import Ubuntu.Components 1.3
20 import Ubuntu.Gestures 0.1
21 import "../Components"
22 import "Indicators"
23 
24 Showable {
25  id: root
26  property alias model: bar.model
27  property alias showDragHandle: __showDragHandle
28  property alias hideDragHandle: __hideDragHandle
29  property alias overFlowWidth: bar.overFlowWidth
30  property alias verticalVelocityThreshold: yVelocityCalculator.velocityThreshold
31  property int minimizedPanelHeight: units.gu(3)
32  property int expandedPanelHeight: units.gu(7)
33  property real openedHeight: units.gu(71)
34  property bool enableHint: true
35  property bool showOnClick: true
36  property color panelColor: theme.palette.normal.background
37  property real menuContentX: 0
38 
39  property alias alignment: bar.alignment
40  property alias hideRow: bar.hideRow
41  property alias rowItemDelegate: bar.rowItemDelegate
42  property alias pageDelegate: content.pageDelegate
43 
44  readonly property real unitProgress: Math.max(0, (height - minimizedPanelHeight) / (openedHeight - minimizedPanelHeight))
45  readonly property bool fullyOpened: unitProgress >= 1
46  readonly property bool partiallyOpened: unitProgress > 0 && unitProgress < 1.0
47  readonly property bool fullyClosed: unitProgress == 0
48  readonly property alias expanded: bar.expanded
49  readonly property int barWidth: bar.width
50  readonly property alias currentMenuIndex: bar.currentItemIndex
51 
52  // Exposes the current contentX of the PanelBar's internal ListView. This
53  // must be used to offset absolute x values against the ListView, since
54  // we commonly add or remove elements and cause the contentX to change.
55  readonly property int rowContentX: bar.rowContentX
56 
57  // The user tapped the panel and did not move.
58  // Note that this does not fire on mouse events, only touch events.
59  signal showTapped()
60 
61  // TODO: Perhaps we need a animation standard for showing/hiding? Each showable seems to
62  // use its own values. Need to ask design about this.
63  showAnimation: SequentialAnimation {
64  StandardAnimation {
65  target: root
66  property: "height"
67  to: openedHeight
68  duration: UbuntuAnimation.BriskDuration
69  easing.type: Easing.OutCubic
70  }
71  // set binding in case units.gu changes while menu open, so height correctly adjusted to fit
72  ScriptAction { script: root.height = Qt.binding( function(){ return root.openedHeight; } ) }
73  }
74 
75  hideAnimation: SequentialAnimation {
76  StandardAnimation {
77  target: root
78  property: "height"
79  to: minimizedPanelHeight
80  duration: UbuntuAnimation.BriskDuration
81  easing.type: Easing.OutCubic
82  }
83  // set binding in case units.gu changes while menu closed, so menu adjusts to fit
84  ScriptAction { script: root.height = Qt.binding( function(){ return root.minimizedPanelHeight; } ) }
85  }
86 
87  shown: false
88  height: minimizedPanelHeight
89 
90  onUnitProgressChanged: d.updateState()
91 
92  Item {
93  anchors {
94  left: parent.left
95  right: parent.right
96  top: bar.bottom
97  bottom: parent.bottom
98  }
99  clip: root.partiallyOpened
100 
101  // eater
102  MouseArea {
103  anchors.fill: content
104  hoverEnabled: true
105  acceptedButtons: Qt.AllButtons
106  onWheel: wheel.accepted = true;
107  enabled: root.state != "initial"
108  visible: content.visible
109  }
110 
111  MenuContent {
112  id: content
113  objectName: "menuContent"
114 
115  anchors {
116  left: parent.left
117  right: parent.right
118  top: parent.top
119  }
120  height: openedHeight - bar.height - handle.height
121  model: root.model
122  visible: root.unitProgress > 0
123  currentMenuIndex: bar.currentItemIndex
124  }
125  }
126 
127  Handle {
128  id: handle
129  objectName: "handle"
130  anchors {
131  left: parent.left
132  right: parent.right
133  bottom: parent.bottom
134  }
135  height: units.gu(2)
136  active: d.activeDragHandle ? true : false
137  visible: !root.fullyClosed
138 
139  //small shadow gradient at bottom of menu
140  Rectangle {
141  anchors {
142  left: parent.left
143  right: parent.right
144  bottom: parent.top
145  }
146  height: units.gu(0.5)
147  gradient: Gradient {
148  GradientStop { position: 0.0; color: "transparent" }
149  GradientStop { position: 1.0; color: theme.palette.normal.background }
150  }
151  opacity: 0.3
152  }
153  }
154 
155  Rectangle {
156  anchors.fill: bar
157  color: panelColor
158  visible: !root.fullyClosed
159  }
160 
161  Keys.onPressed: {
162  if (event.key === Qt.Key_Left) {
163  bar.selectPreviousItem();
164  event.accepted = true;
165  } else if (event.key === Qt.Key_Right) {
166  bar.selectNextItem();
167  event.accepted = true;
168  } else if (event.key === Qt.Key_Escape) {
169  root.hide();
170  event.accepted = true;
171  }
172  }
173 
174  PanelBar {
175  id: bar
176  objectName: "indicatorsBar"
177 
178  anchors {
179  left: parent.left
180  right: parent.right
181  }
182  expanded: false
183  enableLateralChanges: false
184  lateralPosition: -1
185  unitProgress: root.unitProgress
186 
187  height: expanded ? expandedPanelHeight : minimizedPanelHeight
188  Behavior on height { NumberAnimation { duration: UbuntuAnimation.SnapDuration; easing: UbuntuAnimation.StandardEasing } }
189  }
190 
191  ScrollCalculator {
192  id: leftScroller
193  width: units.gu(5)
194  anchors.left: bar.left
195  height: bar.height
196 
197  forceScrollingPercentage: 0.33
198  stopScrollThreshold: units.gu(0.75)
199  direction: Qt.RightToLeft
200  lateralPosition: -1
201 
202  onScroll: bar.addScrollOffset(-scrollAmount);
203  }
204 
205  ScrollCalculator {
206  id: rightScroller
207  width: units.gu(5)
208  anchors.right: bar.right
209  height: bar.height
210 
211  forceScrollingPercentage: 0.33
212  stopScrollThreshold: units.gu(0.75)
213  direction: Qt.LeftToRight
214  lateralPosition: -1
215 
216  onScroll: bar.addScrollOffset(scrollAmount);
217  }
218 
219  MouseArea {
220  anchors.bottom: parent.bottom
221  anchors.left: alignment == Qt.AlignLeft ? parent.left : __showDragHandle.left
222  anchors.right: alignment == Qt.AlignRight ? parent.right : __showDragHandle.right
223  height: minimizedPanelHeight
224  enabled: __showDragHandle.enabled && showOnClick
225  onClicked: {
226  var barPosition = mapToItem(bar, mouseX, mouseY);
227  bar.selectItemAt(barPosition.x)
228  root.show()
229  }
230  }
231 
232  DragHandle {
233  id: __showDragHandle
234  objectName: "showDragHandle"
235  anchors.bottom: parent.bottom
236  anchors.left: alignment == Qt.AlignLeft ? parent.left : undefined
237  anchors.leftMargin: -root.menuContentX
238  anchors.right: alignment == Qt.AlignRight ? parent.right : undefined
239  width: root.overFlowWidth + root.menuContentX
240  height: minimizedPanelHeight
241  direction: Direction.Downwards
242  enabled: !root.shown && root.available && !hideAnimation.running && !showAnimation.running
243  autoCompleteDragThreshold: maxTotalDragDistance / 2
244  stretch: true
245 
246  onPressedChanged: {
247  if (pressed) {
248  touchPressTime = new Date().getTime();
249  } else {
250  var touchReleaseTime = new Date().getTime();
251  if (touchReleaseTime - touchPressTime <= 300 && distance < units.gu(1)) {
252  root.showTapped();
253  }
254  }
255  }
256  property var touchPressTime
257 
258  // using hint regulates minimum to hint displacement, but in fullscreen mode, we need to do it manually.
259  overrideStartValue: enableHint ? minimizedPanelHeight : expandedPanelHeight + handle.height
260  maxTotalDragDistance: openedHeight - (enableHint ? minimizedPanelHeight : expandedPanelHeight + handle.height)
261  hintDisplacement: enableHint ? expandedPanelHeight - minimizedPanelHeight + handle.height : 0
262  }
263 
264  MouseArea {
265  anchors.fill: __hideDragHandle
266  enabled: __hideDragHandle.enabled
267  onClicked: root.hide()
268  }
269 
270  DragHandle {
271  id: __hideDragHandle
272  objectName: "hideDragHandle"
273  anchors.fill: handle
274  direction: Direction.Upwards
275  enabled: root.shown && root.available && !hideAnimation.running && !showAnimation.running
276  hintDisplacement: units.gu(3)
277  autoCompleteDragThreshold: maxTotalDragDistance / 6
278  stretch: true
279  maxTotalDragDistance: openedHeight - expandedPanelHeight - handle.height
280 
281  onTouchPositionChanged: {
282  if (root.state === "locked") {
283  d.xDisplacementSinceLock += (touchPosition.x - d.lastHideTouchX)
284  d.lastHideTouchX = touchPosition.x;
285  }
286  }
287  }
288 
289  PanelVelocityCalculator {
290  id: yVelocityCalculator
291  velocityThreshold: d.hasCommitted ? 0.1 : 0.3
292  trackedValue: d.activeDragHandle ?
293  (Direction.isPositive(d.activeDragHandle.direction) ?
294  d.activeDragHandle.distance :
295  -d.activeDragHandle.distance)
296  : 0
297 
298  onVelocityAboveThresholdChanged: d.updateState()
299  }
300 
301  Connections {
302  target: showAnimation
303  onRunningChanged: {
304  if (showAnimation.running) {
305  root.state = "commit";
306  }
307  }
308  }
309 
310  Connections {
311  target: hideAnimation
312  onRunningChanged: {
313  if (hideAnimation.running) {
314  root.state = "initial";
315  }
316  }
317  }
318 
319  QtObject {
320  id: d
321  property var activeDragHandle: showDragHandle.dragging ? showDragHandle : hideDragHandle.dragging ? hideDragHandle : null
322  property bool hasCommitted: false
323  property real lastHideTouchX: 0
324  property real xDisplacementSinceLock: 0
325  onXDisplacementSinceLockChanged: d.updateState()
326 
327  property real rowMappedLateralPosition: {
328  if (!d.activeDragHandle) return -1;
329  return d.activeDragHandle.mapToItem(bar, d.activeDragHandle.touchPosition.x, 0).x;
330  }
331 
332  function updateState() {
333  if (!showAnimation.running && !hideAnimation.running && d.activeDragHandle) {
334  if (unitProgress <= 0) {
335  root.state = "initial";
336  // lock indicator if we've been committed and aren't moving too much laterally or too fast up.
337  } else if (d.hasCommitted && (Math.abs(d.xDisplacementSinceLock) < units.gu(2) || yVelocityCalculator.velocityAboveThreshold)) {
338  root.state = "locked";
339  } else {
340  root.state = "reveal";
341  }
342  }
343  }
344  }
345 
346  states: [
347  State {
348  name: "initial"
349  PropertyChanges { target: d; hasCommitted: false; restoreEntryValues: false }
350  },
351  State {
352  name: "reveal"
353  StateChangeScript {
354  script: {
355  yVelocityCalculator.reset();
356  // initial item selection
357  if (!d.hasCommitted) bar.selectItemAt(d.rowMappedLateralPosition);
358  d.hasCommitted = false;
359  }
360  }
361  PropertyChanges {
362  target: bar
363  expanded: true
364  // changes to lateral touch position effect which indicator is selected
365  lateralPosition: d.rowMappedLateralPosition
366  // vertical velocity determines if changes in lateral position has an effect
367  enableLateralChanges: d.activeDragHandle &&
368  !yVelocityCalculator.velocityAboveThreshold
369  }
370  // left scroll bar handling
371  PropertyChanges {
372  target: leftScroller
373  lateralPosition: {
374  if (!d.activeDragHandle) return -1;
375  var mapped = d.activeDragHandle.mapToItem(leftScroller, d.activeDragHandle.touchPosition.x, 0);
376  return mapped.x;
377  }
378  }
379  // right scroll bar handling
380  PropertyChanges {
381  target: rightScroller
382  lateralPosition: {
383  if (!d.activeDragHandle) return -1;
384  var mapped = d.activeDragHandle.mapToItem(rightScroller, d.activeDragHandle.touchPosition.x, 0);
385  return mapped.x;
386  }
387  }
388  },
389  State {
390  name: "locked"
391  StateChangeScript {
392  script: {
393  d.xDisplacementSinceLock = 0;
394  d.lastHideTouchX = hideDragHandle.touchPosition.x;
395  }
396  }
397  PropertyChanges { target: bar; expanded: true }
398  },
399  State {
400  name: "commit"
401  extend: "locked"
402  PropertyChanges { target: root; focus: true }
403  PropertyChanges { target: bar; interactive: true }
404  PropertyChanges {
405  target: d;
406  hasCommitted: true
407  lastHideTouchX: 0
408  xDisplacementSinceLock: 0
409  restoreEntryValues: false
410  }
411  }
412  ]
413  state: "initial"
414 }