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  Item {
370  clip: true
371  x: 0
372  y: drawer.y
373  width: drawer.width + drawer.x
374  height: drawer.height
375  BackgroundBlur {
376  id: backgroundBlur
377  x: 0
378  y: 0
379  width: drawer.width
380  height: drawer.height
381  visible: root.interactiveBlur && root.blurSource && drawer.x > -drawer.width
382  sourceItem: root.blurSource
383  blurRect: Qt.rect(0,
384  root.topPanelHeight,
385  drawer.width,
386  drawer.height)
387  occluding: (drawer.width == root.width) && drawer.fullyOpen
388  }
389  }
390 
391  Drawer {
392  id: drawer
393  objectName: "drawer"
394  anchors {
395  top: parent.top
396  topMargin: root.inverted ? root.topPanelHeight : 0
397  bottom: parent.bottom
398  right: parent.left
399  }
400  background: root.background
401  width: Math.min(root.width, units.gu(81))
402  panelWidth: panel.width
403  allowSlidingAnimation: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate
404  staticBlurEnabled: !root.interactiveBlur
405 
406  onApplicationSelected: {
407  root.launcherApplicationSelected(appId)
408  root.hide();
409  root.focus = false;
410  }
411 
412  onHideRequested: {
413  root.hide();
414  }
415 
416  onOpenRequested: {
417  root.toggleDrawer(false, true);
418  }
419 
420  onFullyClosedChanged: {
421  if (!fullyClosed)
422  return
423 
424  drawer.unFocusInput()
425  root.focus = false
426  }
427  }
428 
429  LauncherPanel {
430  id: panel
431  objectName: "launcherPanel"
432  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary" || root.state == "drawer")
433  width: root.panelWidth
434  anchors {
435  top: parent.top
436  bottom: parent.bottom
437  }
438  x: -width
439  visible: root.x > 0 || x > -width || dragArea.pressed
440  model: LauncherModel
441 
442  property var dismissTimer: Timer { interval: 500 }
443  Connections {
444  target: panel.dismissTimer
445  onTriggered: {
446  if (root.state !== "drawer" && root.autohideEnabled && !root.lockedVisible) {
447  if (!edgeBarrier.containsMouse && !panel.preventHiding) {
448  root.state = ""
449  } else {
450  panel.dismissTimer.restart()
451  }
452  }
453  }
454  }
455 
456  property bool animate: true
457 
458  onApplicationSelected: {
459  launcherApplicationSelected(appId);
460  root.hide(ignoreHideIfMouseOverLauncher);
461  }
462  onShowDashHome: {
463  root.hide(ignoreHideIfMouseOverLauncher);
464  root.showDashHome();
465  }
466 
467  onPreventHidingChanged: {
468  if (panel.dismissTimer.running) {
469  panel.dismissTimer.restart();
470  }
471  }
472 
473  onKbdNavigationCancelled: {
474  panel.highlightIndex = -2;
475  root.hide();
476  root.focus = false;
477  }
478 
479  onDraggingChanged: {
480  drawer.unFocusInput()
481  }
482 
483  Behavior on x {
484  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
485  NumberAnimation {
486  duration: 300
487  easing.type: Easing.OutCubic
488  }
489  }
490 
491  Behavior on opacity {
492  NumberAnimation {
493  duration: UbuntuAnimation.FastDuration; easing.type: Easing.OutCubic
494  }
495  }
496  }
497 
498  EdgeBarrier {
499  id: edgeBarrier
500  edge: Qt.LeftEdge
501  target: parent
502  enabled: root.available
503  onProgressChanged: {
504  if (progress > .5 && root.state != "visibleTemporary" && root.state != "drawer" && root.state != "visible") {
505  root.switchToNextState("visibleTemporary");
506  }
507  }
508  onPassed: {
509  if (root.drawerEnabled) {
510  root.toggleDrawer()
511  }
512  }
513 
514  material: Component {
515  Item {
516  Rectangle {
517  width: parent.height
518  height: parent.width
519  rotation: -90
520  anchors.centerIn: parent
521  gradient: Gradient {
522  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
523  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
524  }
525  }
526  }
527  }
528  }
529 
530  SwipeArea {
531  id: dragArea
532  objectName: "launcherDragArea"
533 
534  direction: Direction.Rightwards
535 
536  enabled: root.available
537  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
538  width: root.dragAreaWidth
539  height: root.height
540 
541  function easeInOutCubic(t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }
542 
543  property var lastDragPoints: []
544 
545  function dragDirection() {
546  if (lastDragPoints.length < 5) {
547  return "unknown";
548  }
549 
550  var toRight = true;
551  var toLeft = true;
552  for (var i = lastDragPoints.length - 5; i < lastDragPoints.length; i++) {
553  if (toRight && lastDragPoints[i] < lastDragPoints[i-1]) {
554  toRight = false;
555  }
556  if (toLeft && lastDragPoints[i] > lastDragPoints[i-1]) {
557  toLeft = false;
558  }
559  }
560  return toRight ? "right" : toLeft ? "left" : "unknown";
561  }
562 
563  onDistanceChanged: {
564  if (dragging && launcher.state != "visible" && launcher.state != "drawer") {
565  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
566  }
567 
568  if (root.drawerEnabled && dragging && launcher.state != "drawer") {
569  lastDragPoints.push(distance)
570  var drawerHintDistance = panel.width + units.gu(1)
571  if (distance < drawerHintDistance) {
572  drawer.anchors.rightMargin = -Math.min(Math.max(0, distance), drawer.width);
573  } else {
574  var linearDrawerX = Math.min(Math.max(0, distance - drawerHintDistance), drawer.width);
575  var linearDrawerProgress = linearDrawerX / (drawer.width)
576  var easedDrawerProgress = easeInOutCubic(linearDrawerProgress);
577  drawer.anchors.rightMargin = -(drawerHintDistance + easedDrawerProgress * (drawer.width - drawerHintDistance));
578  }
579  }
580  }
581 
582  onDraggingChanged: {
583  if (!dragging) {
584  if (distance > panel.width / 2) {
585  if (root.drawerEnabled && distance > panel.width * 3 && dragDirection() !== "left") {
586  root.toggleDrawer(false)
587  } else {
588  root.switchToNextState("visible");
589  }
590  } else if (root.state === "") {
591  // didn't drag far enough. rollback
592  root.switchToNextState("");
593  }
594  }
595  lastDragPoints = [];
596  }
597  }
598 
599  states: [
600  State {
601  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
602  PropertyChanges {
603  target: panel
604  restoreEntryValues: false
605  x: -root.panelWidth
606  }
607  PropertyChanges {
608  target: drawer
609  restoreEntryValues: false
610  anchors.rightMargin: 0
611  focus: false
612  }
613  },
614  State {
615  name: "visible"
616  PropertyChanges {
617  target: panel
618  restoreEntryValues: false
619  x: -root.x // so we never go past panelWidth, even when teased by tutorial
620  focus: true
621  }
622  PropertyChanges {
623  target: drawer
624  restoreEntryValues: false
625  anchors.rightMargin: 0
626  focus: false
627  }
628  PropertyChanges {
629  target: root
630  restoreEntryValues: false
631  autohideEnabled: false
632  }
633  },
634  State {
635  name: "drawer"
636  PropertyChanges {
637  target: panel
638  restoreEntryValues: false
639  x: -root.x // so we never go past panelWidth, even when teased by tutorial
640  focus: false
641  }
642  PropertyChanges {
643  target: drawer
644  restoreEntryValues: false
645  anchors.rightMargin: -drawer.width + root.x // so we never go past panelWidth, even when teased by tutorial
646  focus: true
647  }
648  },
649  State {
650  name: "visibleTemporary"
651  extend: "visible"
652  PropertyChanges {
653  target: root
654  restoreEntryValues: false
655  autohideEnabled: true
656  }
657  },
658  State {
659  name: "teasing"
660  when: teaseTimer.running && teaseTimer.mode == "teasing"
661  PropertyChanges {
662  target: panel
663  restoreEntryValues: false
664  x: -root.panelWidth + units.gu(2)
665  }
666  },
667  State {
668  name: "hinting"
669  when: teaseTimer.running && teaseTimer.mode == "hinting"
670  PropertyChanges {
671  target: panel
672  restoreEntryValues: false
673  x: 0
674  }
675  }
676  ]
677 }