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  panel.openQuicklist(panel.highlightIndex)
174  event.accepted = true;
175  break;
176  case Qt.Key_Escape:
177  panel.highlightIndex = -2;
178  // Falling through intentionally
179  case Qt.Key_Enter:
180  case Qt.Key_Return:
181  case Qt.Key_Space:
182  if (panel.highlightIndex == -1) {
183  showDashHome();
184  } else if (panel.highlightIndex >= 0) {
185  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
186  }
187  root.hide();
188  panel.highlightIndex = -2
189  event.accepted = true;
190  root.focus = false;
191  }
192  }
193 
194  Timer {
195  id: superPressTimer
196  interval: 200
197  onTriggered: {
198  switchToNextState("visible")
199  }
200  }
201 
202  Timer {
203  id: superLongPressTimer
204  interval: 1000
205  onTriggered: {
206  switchToNextState("visible")
207  panel.shortcutHintsShown = true;
208  }
209  }
210 
211  Timer {
212  id: teaseTimer
213  interval: mode == "teasing" ? 200 : 300
214  property string mode: "teasing"
215  }
216 
217  // Because the animation on x is disabled while dragging
218  // switching state directly in the drag handlers would not animate
219  // the completion of the hide/reveal gesture. Lets update the state
220  // machine and switch to the final state in the next event loop run
221  Timer {
222  id: animateTimer
223  objectName: "animateTimer"
224  interval: 1
225  property string nextState: ""
226  onTriggered: {
227  if (root.lockedVisible && nextState == "") {
228  // Due to binding updates when switching between modes
229  // it could happen that our request to show will be overwritten
230  // with a hide request. Rewrite it when we know hiding is not allowed.
231  nextState = "visible"
232  }
233 
234  // switching to an intermediate state here to make sure all the
235  // values are restored, even if we were already in the target state
236  root.state = "tmp"
237  root.state = nextState
238  }
239  }
240 
241  Connections {
242  target: LauncherModel
243  onHint: hint();
244  }
245 
246  Connections {
247  target: i18n
248  onLanguageChanged: LauncherModel.refresh()
249  }
250 
251  SequentialAnimation {
252  id: fadeOutAnimation
253  ScriptAction {
254  script: {
255  animateTimer.stop(); // Don't change the state behind our back
256  panel.layer.enabled = true
257  }
258  }
259  UbuntuNumberAnimation {
260  target: panel
261  property: "opacity"
262  easing.type: Easing.InQuad
263  to: 0
264  }
265  ScriptAction {
266  script: {
267  panel.layer.enabled = false
268  panel.animate = false;
269  root.state = "";
270  panel.x = -panel.width
271  panel.opacity = 1;
272  panel.animate = true;
273  }
274  }
275  }
276 
277  InverseMouseArea {
278  id: closeMouseArea
279  anchors.fill: panel
280  enabled: root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
281  visible: enabled
282  onPressed: {
283  mouse.accepted = false;
284  panel.highlightIndex = -2;
285  root.hide();
286  }
287  }
288 
289  MouseArea {
290  id: launcherDragArea
291  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
292  anchors.fill: panel
293  anchors.rightMargin: -units.gu(2)
294  drag {
295  axis: Drag.XAxis
296  maximumX: 0
297  target: panel
298  }
299 
300  onReleased: {
301  if (panel.x < -panel.width/3) {
302  root.switchToNextState("")
303  } else {
304  root.switchToNextState("visible")
305  }
306  }
307  }
308 
309  EdgeBarrier {
310  id: edgeBarrier
311  edge: Qt.LeftEdge
312  target: parent
313  enabled: root.available
314  onPassed: { root.switchToNextState("visibleTemporary"); }
315  material: Component {
316  Item {
317  Rectangle {
318  width: parent.height
319  height: parent.width
320  rotation: -90
321  anchors.centerIn: parent
322  gradient: Gradient {
323  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
324  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
325  }
326  }
327  }
328  }
329  }
330 
331  LauncherPanel {
332  id: panel
333  objectName: "launcherPanel"
334  enabled: root.available && root.state == "visible" || root.state == "visibleTemporary"
335  width: root.panelWidth
336  anchors {
337  top: parent.top
338  bottom: parent.bottom
339  }
340  x: -width
341  visible: root.x > 0 || x > -width || dragArea.pressed
342  model: LauncherModel
343 
344  property var dismissTimer: Timer { interval: 500 }
345  Connections {
346  target: panel.dismissTimer
347  onTriggered: {
348  if (root.autohideEnabled && !root.lockedVisible) {
349  if (!panel.preventHiding) {
350  root.state = ""
351  } else {
352  panel.dismissTimer.restart()
353  }
354  }
355  }
356  }
357 
358  property bool animate: true
359 
360  onApplicationSelected: {
361  root.hide();
362  launcherApplicationSelected(appId)
363  }
364  onShowDashHome: {
365  root.hide();
366  root.showDashHome();
367  }
368 
369  onPreventHidingChanged: {
370  if (panel.dismissTimer.running) {
371  panel.dismissTimer.restart();
372  }
373  }
374 
375  onKbdNavigationCancelled: {
376  panel.highlightIndex = -2;
377  root.hide();
378  root.focus = false;
379  }
380 
381  Behavior on x {
382  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
383  NumberAnimation {
384  duration: 300
385  easing.type: Easing.OutCubic
386  }
387  }
388 
389  Behavior on opacity {
390  NumberAnimation {
391  duration: UbuntuAnimation.FastDuration; easing.type: Easing.OutCubic
392  }
393  }
394  }
395 
396  SwipeArea {
397  id: dragArea
398  objectName: "launcherDragArea"
399 
400  direction: Direction.Rightwards
401 
402  enabled: root.available
403  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
404  width: root.dragAreaWidth
405  height: root.height
406 
407  onDistanceChanged: {
408  if (!dragging || launcher.state == "visible")
409  return;
410 
411  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
412  }
413 
414  onDraggingChanged: {
415  if (!dragging) {
416  if (distance > panel.width / 2) {
417  root.switchToNextState("visible")
418  if (distance > minimizeDistance) {
419  root.dash();
420  }
421  } else if (root.state === "") {
422  // didn't drag far enough. rollback
423  root.switchToNextState("")
424  }
425  }
426  }
427  }
428 
429  states: [
430  State {
431  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
432  PropertyChanges {
433  target: panel
434  x: -root.panelWidth
435  }
436  },
437  State {
438  name: "visible"
439  PropertyChanges {
440  target: panel
441  x: -root.x // so we never go past panelWidth, even when teased by tutorial
442  }
443  },
444  State {
445  name: "visibleTemporary"
446  extend: "visible"
447  PropertyChanges {
448  target: root
449  autohideEnabled: true
450  }
451  },
452  State {
453  name: "teasing"
454  when: teaseTimer.running && teaseTimer.mode == "teasing"
455  PropertyChanges {
456  target: panel
457  x: -root.panelWidth + units.gu(2)
458  }
459  },
460  State {
461  name: "hinting"
462  when: teaseTimer.running && teaseTimer.mode == "hinting"
463  PropertyChanges {
464  target: panel
465  x: 0
466  }
467  }
468  ]
469 }