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 
23 FocusScope {
24  id: root
25 
26  property bool autohideEnabled: false
27  property bool lockedVisible: false
28  property bool available: true // can be used to disable all interactions
29  property alias inverted: panel.inverted
30 
31  property int panelWidth: units.gu(10)
32  property int dragAreaWidth: units.gu(1)
33  property int minimizeDistance: units.gu(26)
34  property real progress: dragArea.dragging && dragArea.touchPosition.x > panelWidth ?
35  (width * (dragArea.touchPosition.x-panelWidth) / (width - panelWidth)) : 0
36 
37  property bool superPressed: false
38  property bool superTabPressed: false
39 
40  readonly property bool dragging: dragArea.dragging
41  readonly property real dragDistance: dragArea.dragging ? dragArea.touchPosition.x : 0
42  readonly property real visibleWidth: panel.width + panel.x
43 
44  readonly property bool shown: panel.x > -panel.width
45 
46  // emitted when an application is selected
47  signal launcherApplicationSelected(string appId)
48 
49  // emitted when the apps dash should be shown because of a swipe gesture
50  signal dash()
51 
52  // emitted when the dash icon in the launcher has been tapped
53  signal showDashHome()
54 
55  onStateChanged: {
56  if (state == "") {
57  panel.dismissTimer.stop()
58  } else {
59  panel.dismissTimer.restart()
60  }
61  }
62 
63  onSuperPressedChanged: {
64  if (superPressed) {
65  superPressTimer.start();
66  superLongPressTimer.start();
67  } else {
68  superPressTimer.stop();
69  superLongPressTimer.stop();
70  launcher.switchToNextState("");
71  panel.shortcutHintsShown = false;
72  }
73  }
74 
75  onSuperTabPressedChanged: {
76  if (superTabPressed) {
77  switchToNextState("visible")
78  panel.highlightIndex = -1;
79  root.focus = true;
80  superPressTimer.stop();
81  superLongPressTimer.stop();
82  } else {
83  if (panel.highlightIndex == -1) {
84  showDashHome();
85  } else if (panel.highlightIndex >= 0){
86  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
87  }
88  panel.highlightIndex = -2;
89  switchToNextState("");
90  root.focus = false;
91  }
92  }
93 
94  onLockedVisibleChanged: {
95  if (lockedVisible && state == "") {
96  panel.dismissTimer.stop();
97  fadeOutAnimation.stop();
98  switchToNextState("visible")
99  } else if (!lockedVisible && state == "visible") {
100  hide();
101  }
102  }
103 
104  function hide() {
105  switchToNextState("")
106  }
107 
108  function fadeOut() {
109  if (!root.lockedVisible) {
110  fadeOutAnimation.start();
111  }
112  }
113 
114  function switchToNextState(state) {
115  animateTimer.nextState = state
116  animateTimer.start();
117  }
118 
119  function tease() {
120  if (available && !dragArea.dragging) {
121  teaseTimer.mode = "teasing"
122  teaseTimer.start();
123  }
124  }
125 
126  function hint() {
127  if (available && root.state == "") {
128  teaseTimer.mode = "hinting"
129  teaseTimer.start();
130  }
131  }
132 
133  function pushEdge(amount) {
134  if (root.state === "") {
135  edgeBarrier.push(amount);
136  }
137  }
138 
139  function openForKeyboardNavigation() {
140  panel.highlightIndex = -1; // The BFB
141  root.focus = true;
142  switchToNextState("visible")
143  }
144 
145  Keys.onPressed: {
146  switch (event.key) {
147  case Qt.Key_Backtab:
148  panel.highlightPrevious();
149  event.accepted = true;
150  break;
151  case Qt.Key_Up:
152  if (root.inverted) {
153  panel.highlightNext()
154  } else {
155  panel.highlightPrevious();
156  }
157  event.accepted = true;
158  break;
159  case Qt.Key_Tab:
160  panel.highlightNext();
161  event.accepted = true;
162  break;
163  case Qt.Key_Down:
164  if (root.inverted) {
165  panel.highlightPrevious();
166  } else {
167  panel.highlightNext();
168  }
169  event.accepted = true;
170  break;
171  case Qt.Key_Right:
172  panel.openQuicklist(panel.highlightIndex)
173  event.accepted = true;
174  break;
175  case Qt.Key_Escape:
176  panel.highlightIndex = -2;
177  // Falling through intentionally
178  case Qt.Key_Enter:
179  case Qt.Key_Return:
180  case Qt.Key_Space:
181  if (panel.highlightIndex == -1) {
182  showDashHome();
183  } else if (panel.highlightIndex >= 0) {
184  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
185  }
186  root.hide();
187  panel.highlightIndex = -2
188  event.accepted = true;
189  root.focus = false;
190  }
191  }
192 
193  Timer {
194  id: superPressTimer
195  interval: 200
196  onTriggered: {
197  switchToNextState("visible")
198  }
199  }
200 
201  Timer {
202  id: superLongPressTimer
203  interval: 1000
204  onTriggered: {
205  switchToNextState("visible")
206  panel.shortcutHintsShown = true;
207  }
208  }
209 
210  Timer {
211  id: teaseTimer
212  interval: mode == "teasing" ? 200 : 300
213  property string mode: "teasing"
214  }
215 
216  // Because the animation on x is disabled while dragging
217  // switching state directly in the drag handlers would not animate
218  // the completion of the hide/reveal gesture. Lets update the state
219  // machine and switch to the final state in the next event loop run
220  Timer {
221  id: animateTimer
222  objectName: "animateTimer"
223  interval: 1
224  property string nextState: ""
225  onTriggered: {
226  if (root.lockedVisible && nextState == "") {
227  // Due to binding updates when switching between modes
228  // it could happen that our request to show will be overwritten
229  // with a hide request. Rewrite it when we know hiding is not allowed.
230  nextState = "visible"
231  }
232 
233  // switching to an intermediate state here to make sure all the
234  // values are restored, even if we were already in the target state
235  root.state = "tmp"
236  root.state = nextState
237  }
238  }
239 
240  Connections {
241  target: LauncherModel
242  onHint: hint();
243  }
244 
245  Connections {
246  target: i18n
247  onLanguageChanged: LauncherModel.refresh()
248  }
249 
250  SequentialAnimation {
251  id: fadeOutAnimation
252  ScriptAction {
253  script: {
254  animateTimer.stop(); // Don't change the state behind our back
255  panel.layer.enabled = true
256  }
257  }
258  UbuntuNumberAnimation {
259  target: panel
260  property: "opacity"
261  easing.type: Easing.InQuad
262  to: 0
263  }
264  ScriptAction {
265  script: {
266  panel.layer.enabled = false
267  panel.animate = false;
268  root.state = "";
269  panel.x = -panel.width
270  panel.opacity = 1;
271  panel.animate = true;
272  }
273  }
274  }
275 
276  InverseMouseArea {
277  id: closeMouseArea
278  anchors.fill: panel
279  enabled: root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
280  visible: enabled
281  onPressed: {
282  mouse.accepted = false;
283  panel.highlightIndex = -2;
284  root.hide();
285  }
286  }
287 
288  MouseArea {
289  id: launcherDragArea
290  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
291  anchors.fill: panel
292  anchors.rightMargin: -units.gu(2)
293  drag {
294  axis: Drag.XAxis
295  maximumX: 0
296  target: panel
297  }
298 
299  onReleased: {
300  if (panel.x < -panel.width/3) {
301  root.switchToNextState("")
302  } else {
303  root.switchToNextState("visible")
304  }
305  }
306  }
307 
308  EdgeBarrier {
309  id: edgeBarrier
310  edge: Qt.LeftEdge
311  target: parent
312  enabled: root.available
313  onPassed: { root.switchToNextState("visibleTemporary"); }
314  material: Component {
315  Item {
316  Rectangle {
317  width: parent.height
318  height: parent.width
319  rotation: -90
320  anchors.centerIn: parent
321  gradient: Gradient {
322  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
323  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
324  }
325  }
326  }
327  }
328  }
329 
330  LauncherPanel {
331  id: panel
332  objectName: "launcherPanel"
333  enabled: root.available && root.state == "visible" || root.state == "visibleTemporary"
334  width: root.panelWidth
335  anchors {
336  top: parent.top
337  bottom: parent.bottom
338  }
339  x: -width
340  visible: root.x > 0 || x > -width || dragArea.pressed
341  model: LauncherModel
342 
343  property var dismissTimer: Timer { interval: 500 }
344  Connections {
345  target: panel.dismissTimer
346  onTriggered: {
347  if (root.autohideEnabled && !root.lockedVisible) {
348  if (!panel.preventHiding) {
349  root.state = ""
350  } else {
351  panel.dismissTimer.restart()
352  }
353  }
354  }
355  }
356 
357  property bool animate: true
358 
359  onApplicationSelected: {
360  root.hide();
361  launcherApplicationSelected(appId)
362  }
363  onShowDashHome: {
364  root.hide();
365  root.showDashHome();
366  }
367 
368  onPreventHidingChanged: {
369  if (panel.dismissTimer.running) {
370  panel.dismissTimer.restart();
371  }
372  }
373 
374  onKbdNavigationCancelled: {
375  panel.highlightIndex = -2;
376  root.hide();
377  root.focus = false;
378  }
379 
380  Behavior on x {
381  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
382  NumberAnimation {
383  duration: 300
384  easing.type: Easing.OutCubic
385  }
386  }
387 
388  Behavior on opacity {
389  NumberAnimation {
390  duration: UbuntuAnimation.FastDuration; easing.type: Easing.OutCubic
391  }
392  }
393  }
394 
395  SwipeArea {
396  id: dragArea
397  objectName: "launcherDragArea"
398 
399  direction: Direction.Rightwards
400 
401  enabled: root.available
402  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
403  width: root.dragAreaWidth
404  height: root.height
405 
406  onDistanceChanged: {
407  if (!dragging || launcher.state == "visible")
408  return;
409 
410  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
411  }
412 
413  onDraggingChanged: {
414  if (!dragging) {
415  if (distance > panel.width / 2) {
416  root.switchToNextState("visible")
417  if (distance > minimizeDistance) {
418  root.dash();
419  }
420  } else if (root.state === "") {
421  // didn't drag far enough. rollback
422  root.switchToNextState("")
423  }
424  }
425  }
426  }
427 
428  states: [
429  State {
430  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
431  PropertyChanges {
432  target: panel
433  x: -root.panelWidth
434  }
435  },
436  State {
437  name: "visible"
438  PropertyChanges {
439  target: panel
440  x: -root.x // so we never go past panelWidth, even when teased by tutorial
441  }
442  },
443  State {
444  name: "visibleTemporary"
445  extend: "visible"
446  PropertyChanges {
447  target: root
448  autohideEnabled: true
449  }
450  },
451  State {
452  name: "teasing"
453  when: teaseTimer.running && teaseTimer.mode == "teasing"
454  PropertyChanges {
455  target: panel
456  x: -root.panelWidth + units.gu(2)
457  }
458  },
459  State {
460  name: "hinting"
461  when: teaseTimer.running && teaseTimer.mode == "hinting"
462  PropertyChanges {
463  target: panel
464  x: 0
465  }
466  }
467  ]
468 }