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