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