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