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