Lomiri
Launcher.qml
1 /*
2  * Copyright (C) 2013-2015 Canonical Ltd.
3  * Copyright (C) 2021 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 "../Components"
20 import Lomiri.Components 1.3
21 import Lomiri.Gestures 0.1
22 import Lomiri.Launcher 0.1
23 import Utils 0.1 as Utils
24 
25 FocusScope {
26  id: root
27 
28  readonly property int ignoreHideIfMouseOverLauncher: 1
29 
30  property bool autohideEnabled: false
31  property bool lockedVisible: false
32  property bool available: true // can be used to disable all interactions
33  property alias inverted: panel.inverted
34  property Item blurSource: null
35  property int topPanelHeight: 0
36  property bool drawerEnabled: true
37  property alias privateMode: panel.privateMode
38  property url background
39 
40  property int panelWidth: units.gu(10)
41  property int dragAreaWidth: units.gu(1)
42  property real progress: dragArea.dragging && dragArea.touchPosition.x > panelWidth ?
43  (width * (dragArea.touchPosition.x-panelWidth) / (width - panelWidth)) : 0
44 
45  property bool superPressed: false
46  property bool superTabPressed: false
47  property bool takesFocus: false;
48 
49  readonly property bool dragging: dragArea.dragging
50  readonly property real dragDistance: dragArea.dragging ? dragArea.touchPosition.x : 0
51  readonly property real visibleWidth: panel.width + panel.x
52  readonly property alias shortcutHintsShown: panel.shortcutHintsShown
53 
54  readonly property bool shown: panel.x > -panel.width
55  readonly property bool drawerShown: drawer.x == 0
56 
57  // emitted when an application is selected
58  signal launcherApplicationSelected(string appId)
59 
60  // emitted when the dash icon in the launcher has been tapped
61  signal showDashHome()
62 
63  onStateChanged: {
64  if (state == "") {
65  panel.dismissTimer.stop()
66  } else {
67  panel.dismissTimer.restart()
68  }
69  }
70 
71  onFocusChanged: {if (!focus) { root.takesFocus = false; }}
72 
73  onSuperPressedChanged: {
74  if (state == "drawer")
75  return;
76 
77  if (superPressed) {
78  superPressTimer.start();
79  superLongPressTimer.start();
80  } else {
81  superPressTimer.stop();
82  superLongPressTimer.stop();
83  switchToNextState(root.lockedVisible ? "visible" : "");
84  panel.shortcutHintsShown = false;
85  }
86  }
87 
88  onSuperTabPressedChanged: {
89  if (superTabPressed) {
90  switchToNextState("visible")
91  panel.highlightIndex = -1;
92  root.takesFocus = true;
93  root.focus = true;
94  superPressTimer.stop();
95  superLongPressTimer.stop();
96  } else {
97  switchToNextState(root.lockedVisible ? "visible" : "");
98  root.focus = false;
99  if (panel.highlightIndex == -1) {
100  root.showDashHome();
101  } else if (panel.highlightIndex >= 0){
102  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
103  }
104  panel.highlightIndex = -2;
105  }
106  }
107 
108  onLockedVisibleChanged: {
109  // We are in the progress of moving to the drawer
110  // this is caused by the user pressing the bfb on unlock
111  // in this case we want to show the drawer and not
112  // just visible
113  if (animateTimer.nextState == "drawer")
114  return;
115 
116  if (lockedVisible && state == "") {
117  panel.dismissTimer.stop();
118  fadeOutAnimation.stop();
119  switchToNextState("visible")
120  } else if (!lockedVisible && (state == "visible" || state == "drawer")) {
121  hide();
122  }
123  }
124 
125  onPanelWidthChanged: {
126  hint();
127  }
128 
129  // Switches the Launcher to the visible state, but only if it's not already
130  // opened.
131  // Prevents closing the Drawer when trying to show the Launcher.
132  function show() {
133  if (state === "" || state === "visibleTemporary") {
134  switchToNextState("visible");
135  }
136  }
137 
138  function hide(flags) {
139  if ((flags & ignoreHideIfMouseOverLauncher) && Utils.Functions.itemUnderMouse(panel)) {
140  if (state == "drawer") {
141  switchToNextState("visibleTemporary");
142  }
143  return;
144  }
145  if (root.lockedVisible) {
146  // Due to binding updates when switching between modes
147  // it could happen that our request to show will be overwritten
148  // with a hide request. Rewrite it when we know hiding is not allowed.
149  switchToNextState("visible")
150  } else {
151  switchToNextState("")
152  }
153  root.focus = false;
154  }
155 
156  function fadeOut() {
157  if (!root.lockedVisible) {
158  fadeOutAnimation.start();
159  }
160  }
161 
162  function switchToNextState(state) {
163  animateTimer.nextState = state
164  animateTimer.start();
165  }
166 
167  function tease() {
168  if (available && !dragArea.dragging) {
169  teaseTimer.mode = "teasing"
170  teaseTimer.start();
171  }
172  }
173 
174  function hint() {
175  if (available && root.state == "") {
176  teaseTimer.mode = "hinting"
177  teaseTimer.start();
178  }
179  }
180 
181  function pushEdge(amount) {
182  if (root.state === "" || root.state == "visible" || root.state == "visibleTemporary") {
183  edgeBarrier.push(amount);
184  }
185  }
186 
187  function openForKeyboardNavigation() {
188  panel.highlightIndex = -1; // The BFB
189  drawer.focus = false;
190  root.takesFocus = true;
191  root.focus = true;
192  switchToNextState("visible")
193  }
194 
195  function toggleDrawer(focusInputField, onlyOpen, alsoToggleLauncher) {
196  if (!drawerEnabled) {
197  return;
198  }
199 
200  panel.shortcutHintsShown = false;
201  superPressTimer.stop();
202  superLongPressTimer.stop();
203  root.takesFocus = true;
204  root.focus = true;
205  if (focusInputField) {
206  drawer.focusInput();
207  }
208  if (state === "drawer" && !onlyOpen)
209  if (alsoToggleLauncher && !root.lockedVisible)
210  switchToNextState("");
211  else
212  switchToNextState("visible");
213  else
214  switchToNextState("drawer");
215  }
216 
217  Keys.onPressed: {
218  switch (event.key) {
219  case Qt.Key_Backtab:
220  panel.highlightPrevious();
221  event.accepted = true;
222  break;
223  case Qt.Key_Up:
224  if (root.inverted) {
225  panel.highlightNext()
226  } else {
227  panel.highlightPrevious();
228  }
229  event.accepted = true;
230  break;
231  case Qt.Key_Tab:
232  panel.highlightNext();
233  event.accepted = true;
234  break;
235  case Qt.Key_Down:
236  if (root.inverted) {
237  panel.highlightPrevious();
238  } else {
239  panel.highlightNext();
240  }
241  event.accepted = true;
242  break;
243  case Qt.Key_Right:
244  case Qt.Key_Menu:
245  panel.openQuicklist(panel.highlightIndex)
246  event.accepted = true;
247  break;
248  case Qt.Key_Escape:
249  panel.highlightIndex = -2;
250  // Falling through intentionally
251  case Qt.Key_Enter:
252  case Qt.Key_Return:
253  case Qt.Key_Space:
254  if (panel.highlightIndex == -1) {
255  root.showDashHome();
256  } else if (panel.highlightIndex >= 0) {
257  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
258  }
259  root.hide();
260  panel.highlightIndex = -2
261  event.accepted = true;
262  }
263  }
264 
265  Timer {
266  id: superPressTimer
267  interval: 200
268  onTriggered: {
269  switchToNextState("visible")
270  }
271  }
272 
273  Timer {
274  id: superLongPressTimer
275  interval: 1000
276  onTriggered: {
277  switchToNextState("visible")
278  panel.shortcutHintsShown = true;
279  }
280  }
281 
282  Timer {
283  id: teaseTimer
284  interval: mode == "teasing" ? 200 : 300
285  property string mode: "teasing"
286  }
287 
288  // Because the animation on x is disabled while dragging
289  // switching state directly in the drag handlers would not animate
290  // the completion of the hide/reveal gesture. Lets update the state
291  // machine and switch to the final state in the next event loop run
292  Timer {
293  id: animateTimer
294  objectName: "animateTimer"
295  interval: 1
296  property string nextState: ""
297  onTriggered: {
298  // switching to an intermediate state here to make sure all the
299  // values are restored, even if we were already in the target state
300  root.state = "tmp"
301  root.state = nextState
302  }
303  }
304 
305  Connections {
306  target: LauncherModel
307  onHint: hint();
308  }
309 
310  Connections {
311  target: i18n
312  onLanguageChanged: LauncherModel.refresh()
313  }
314 
315  SequentialAnimation {
316  id: fadeOutAnimation
317  ScriptAction {
318  script: {
319  animateTimer.stop(); // Don't change the state behind our back
320  panel.layer.enabled = true
321  }
322  }
323  LomiriNumberAnimation {
324  target: panel
325  property: "opacity"
326  easing.type: Easing.InQuad
327  to: 0
328  }
329  ScriptAction {
330  script: {
331  panel.layer.enabled = false
332  panel.animate = false;
333  root.state = "";
334  panel.x = -panel.width
335  panel.opacity = 1;
336  panel.animate = true;
337  }
338  }
339  }
340 
341  InverseMouseArea {
342  id: closeMouseArea
343  anchors.fill: panel
344  enabled: (root.state == "visible" && !root.lockedVisible) || root.state == "drawer" || hoverEnabled
345  hoverEnabled: panel.quickListOpen
346  visible: enabled
347  onPressed: {
348  mouse.accepted = false;
349  panel.highlightIndex = -2;
350  root.hide();
351  }
352  }
353 
354  MouseArea {
355  id: launcherDragArea
356  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
357  anchors.fill: panel
358  anchors.rightMargin: -units.gu(2)
359  drag {
360  axis: Drag.XAxis
361  maximumX: 0
362  target: panel
363  }
364 
365  onReleased: {
366  if (panel.x < -panel.width/3) {
367  root.switchToNextState("")
368  } else {
369  root.switchToNextState("visible")
370  }
371  }
372  }
373 
374  Item {
375  clip: true
376  x: 0
377  y: drawer.y
378  width: drawer.width + drawer.x
379  height: drawer.height
380  BackgroundBlur {
381  id: backgroundBlur
382  x: 0
383  y: 0
384  width: drawer.width
385  height: drawer.height
386  visible: drawer.x > -drawer.width
387  sourceItem: root.blurSource
388  blurRect: Qt.rect(0,
389  root.topPanelHeight,
390  drawer.width,
391  drawer.height)
392  occluding: (drawer.width == root.width) && drawer.fullyOpen
393  }
394  }
395 
396  Drawer {
397  id: drawer
398  objectName: "drawer"
399  anchors {
400  top: parent.top
401  topMargin: root.inverted ? root.topPanelHeight : 0
402  bottom: parent.bottom
403  right: parent.left
404  }
405  background: root.background
406  width: Math.min(root.width, units.gu(81))
407  panelWidth: panel.width
408  allowSlidingAnimation: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate
409 
410  onApplicationSelected: {
411  root.launcherApplicationSelected(appId)
412  root.hide();
413  root.focus = false;
414  }
415 
416  onHideRequested: {
417  root.hide();
418  }
419 
420  onOpenRequested: {
421  root.toggleDrawer(false, true);
422  }
423 
424  onFullyClosedChanged: {
425  if (!fullyClosed)
426  return
427 
428  drawer.unFocusInput()
429  root.focus = false
430  }
431  }
432 
433  LauncherPanel {
434  id: panel
435  objectName: "launcherPanel"
436  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary" || root.state == "drawer")
437  width: root.panelWidth
438  anchors {
439  top: parent.top
440  bottom: parent.bottom
441  }
442  x: -width
443  visible: root.x > 0 || x > -width || dragArea.pressed
444  model: LauncherModel
445 
446  property var dismissTimer: Timer { interval: 500 }
447  Connections {
448  target: panel.dismissTimer
449  onTriggered: {
450  if (root.state !== "drawer" && root.autohideEnabled && !root.lockedVisible) {
451  if (!edgeBarrier.containsMouse && !panel.preventHiding) {
452  root.state = ""
453  } else {
454  panel.dismissTimer.restart()
455  }
456  }
457  }
458  }
459 
460  property bool animate: true
461 
462  onApplicationSelected: {
463  launcherApplicationSelected(appId);
464  root.hide(ignoreHideIfMouseOverLauncher);
465  }
466  onShowDashHome: {
467  root.hide(ignoreHideIfMouseOverLauncher);
468  root.showDashHome();
469  }
470 
471  onPreventHidingChanged: {
472  if (panel.dismissTimer.running) {
473  panel.dismissTimer.restart();
474  }
475  }
476 
477  onKbdNavigationCancelled: {
478  panel.highlightIndex = -2;
479  root.hide();
480  root.focus = false;
481  }
482 
483  onDraggingChanged: {
484  drawer.unFocusInput()
485  }
486 
487  Behavior on x {
488  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
489  NumberAnimation {
490  duration: 300
491  easing.type: Easing.OutCubic
492  }
493  }
494 
495  Behavior on opacity {
496  NumberAnimation {
497  duration: LomiriAnimation.FastDuration; easing.type: Easing.OutCubic
498  }
499  }
500  }
501 
502  EdgeBarrier {
503  id: edgeBarrier
504  edge: Qt.LeftEdge
505  target: parent
506  enabled: root.available
507  onProgressChanged: {
508  if (progress > .5 && root.state != "visibleTemporary" && root.state != "drawer" && root.state != "visible") {
509  root.switchToNextState("visibleTemporary");
510  }
511  }
512  onPassed: {
513  if (root.drawerEnabled) {
514  root.toggleDrawer()
515  }
516  }
517 
518  material: Component {
519  Item {
520  Rectangle {
521  width: parent.height
522  height: parent.width
523  rotation: -90
524  anchors.centerIn: parent
525  gradient: Gradient {
526  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
527  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
528  }
529  }
530  }
531  }
532  }
533 
534  SwipeArea {
535  id: dragArea
536  objectName: "launcherDragArea"
537 
538  direction: Direction.Rightwards
539 
540  enabled: root.available
541  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
542  width: root.dragAreaWidth
543  height: root.height
544 
545  function easeInOutCubic(t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }
546 
547  property var lastDragPoints: []
548 
549  function dragDirection() {
550  if (lastDragPoints.length < 5) {
551  return "unknown";
552  }
553 
554  var toRight = true;
555  var toLeft = true;
556  for (var i = lastDragPoints.length - 5; i < lastDragPoints.length; i++) {
557  if (toRight && lastDragPoints[i] < lastDragPoints[i-1]) {
558  toRight = false;
559  }
560  if (toLeft && lastDragPoints[i] > lastDragPoints[i-1]) {
561  toLeft = false;
562  }
563  }
564  return toRight ? "right" : toLeft ? "left" : "unknown";
565  }
566 
567  onDistanceChanged: {
568  if (dragging && launcher.state != "visible" && launcher.state != "drawer") {
569  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
570  }
571 
572  if (root.drawerEnabled && dragging && launcher.state != "drawer") {
573  lastDragPoints.push(distance)
574  var drawerHintDistance = panel.width + units.gu(1)
575  if (distance < drawerHintDistance) {
576  drawer.anchors.rightMargin = -Math.min(Math.max(0, distance), drawer.width);
577  } else {
578  var linearDrawerX = Math.min(Math.max(0, distance - drawerHintDistance), drawer.width);
579  var linearDrawerProgress = linearDrawerX / (drawer.width)
580  var easedDrawerProgress = easeInOutCubic(linearDrawerProgress);
581  drawer.anchors.rightMargin = -(drawerHintDistance + easedDrawerProgress * (drawer.width - drawerHintDistance));
582  }
583  }
584  }
585 
586  onDraggingChanged: {
587  if (!dragging) {
588  if (distance > panel.width / 2) {
589  if (root.drawerEnabled && distance > panel.width * 3 && dragDirection() !== "left") {
590  root.toggleDrawer(false)
591  } else {
592  root.switchToNextState("visible");
593  }
594  } else if (root.state === "") {
595  // didn't drag far enough. rollback
596  root.switchToNextState("");
597  }
598  }
599  lastDragPoints = [];
600  }
601  }
602 
603  states: [
604  State {
605  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
606  PropertyChanges {
607  target: panel
608  restoreEntryValues: false
609  x: -root.panelWidth
610  }
611  PropertyChanges {
612  target: drawer
613  restoreEntryValues: false
614  anchors.rightMargin: 0
615  focus: false
616  }
617  },
618  State {
619  name: "visible"
620  PropertyChanges {
621  target: panel
622  restoreEntryValues: false
623  x: -root.x // so we never go past panelWidth, even when teased by tutorial
624  focus: true
625  }
626  PropertyChanges {
627  target: drawer
628  restoreEntryValues: false
629  anchors.rightMargin: 0
630  focus: false
631  }
632  PropertyChanges {
633  target: root
634  restoreEntryValues: false
635  autohideEnabled: false
636  }
637  },
638  State {
639  name: "drawer"
640  PropertyChanges {
641  target: panel
642  restoreEntryValues: false
643  x: -root.x // so we never go past panelWidth, even when teased by tutorial
644  focus: false
645  }
646  PropertyChanges {
647  target: drawer
648  restoreEntryValues: false
649  anchors.rightMargin: -drawer.width + root.x // so we never go past panelWidth, even when teased by tutorial
650  focus: true
651  }
652  },
653  State {
654  name: "visibleTemporary"
655  extend: "visible"
656  PropertyChanges {
657  target: root
658  restoreEntryValues: false
659  autohideEnabled: true
660  }
661  },
662  State {
663  name: "teasing"
664  when: teaseTimer.running && teaseTimer.mode == "teasing"
665  PropertyChanges {
666  target: panel
667  restoreEntryValues: false
668  x: -root.panelWidth + units.gu(2)
669  }
670  },
671  State {
672  name: "hinting"
673  when: teaseTimer.running && teaseTimer.mode == "hinting"
674  PropertyChanges {
675  target: panel
676  restoreEntryValues: false
677  x: 0
678  }
679  }
680  ]
681 }