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