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