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 GlobalShortcut 1.0
23 
24 FocusScope {
25  id: root
26 
27  property bool autohideEnabled: false
28  property bool lockedVisible: false
29  property bool available: true // can be used to disable all interactions
30  property alias inverted: panel.inverted
31 
32  property int panelWidth: units.gu(10)
33  property int dragAreaWidth: units.gu(1)
34  property int minimizeDistance: units.gu(26)
35  property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
36  (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
37 
38  property bool superPressed: false
39  property bool superTabPressed: false
40 
41  readonly property bool dragging: dragArea.dragging
42  readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
43  readonly property real visibleWidth: panel.width + panel.x
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  MouseArea {
278  id: launcherDragArea
279  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
280  anchors.fill: panel
281  anchors.rightMargin: -units.gu(2)
282  drag {
283  axis: Drag.XAxis
284  maximumX: 0
285  target: panel
286  }
287 
288  onReleased: {
289  if (panel.x < -panel.width/3) {
290  root.switchToNextState("")
291  } else {
292  root.switchToNextState("visible")
293  }
294  }
295  }
296 
297  InverseMouseArea {
298  id: closeMouseArea
299  anchors.fill: panel
300  enabled: root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
301  visible: enabled
302  onPressed: {
303  panel.highlightIndex = -2
304  root.hide();
305  }
306  }
307 
308  Rectangle {
309  id: backgroundShade
310  anchors.fill: parent
311  color: "black"
312  opacity: root.state == "visible" && !root.lockedVisible ? 0.6 : 0
313 
314  Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.BriskDuration } }
315  }
316 
317  EdgeBarrier {
318  id: edgeBarrier
319  edge: Qt.LeftEdge
320  target: parent
321  enabled: root.available
322  onPassed: { root.switchToNextState("visibleTemporary"); }
323  material: Component {
324  Item {
325  Rectangle {
326  width: parent.height
327  height: parent.width
328  rotation: -90
329  anchors.centerIn: parent
330  gradient: Gradient {
331  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
332  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
333  }
334  }
335  }
336  }
337  }
338 
339  LauncherPanel {
340  id: panel
341  objectName: "launcherPanel"
342  enabled: root.available && root.state == "visible" || root.state == "visibleTemporary"
343  width: root.panelWidth
344  anchors {
345  top: parent.top
346  bottom: parent.bottom
347  }
348  x: -width
349  visible: root.x > 0 || x > -width || dragArea.pressed
350  model: LauncherModel
351 
352  property var dismissTimer: Timer { interval: 500 }
353  Connections {
354  target: panel.dismissTimer
355  onTriggered: {
356  if (root.autohideEnabled && !root.lockedVisible) {
357  if (!panel.preventHiding) {
358  root.state = ""
359  } else {
360  panel.dismissTimer.restart()
361  }
362  }
363  }
364  }
365 
366  property bool animate: true
367 
368  onApplicationSelected: {
369  root.hide();
370  launcherApplicationSelected(appId)
371  }
372  onShowDashHome: {
373  root.hide();
374  root.showDashHome();
375  }
376 
377  onPreventHidingChanged: {
378  if (panel.dismissTimer.running) {
379  panel.dismissTimer.restart();
380  }
381  }
382 
383  onKbdNavigationCancelled: {
384  panel.highlightIndex = -2;
385  root.hide();
386  root.focus = false;
387  }
388 
389  Behavior on x {
390  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
391  NumberAnimation {
392  duration: 300
393  easing.type: Easing.OutCubic
394  }
395  }
396 
397  Behavior on opacity {
398  NumberAnimation {
399  duration: UbuntuAnimation.FastDuration; easing.type: Easing.OutCubic
400  }
401  }
402  }
403 
404  DirectionalDragArea {
405  id: dragArea
406  objectName: "launcherDragArea"
407 
408  direction: Direction.Rightwards
409 
410  enabled: root.available
411  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
412  width: root.dragAreaWidth
413  height: root.height
414 
415  onDistanceChanged: {
416  if (!dragging || launcher.state == "visible")
417  return;
418 
419  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
420  }
421 
422  onDraggingChanged: {
423  if (!dragging) {
424  if (distance > panel.width / 2) {
425  root.switchToNextState("visible")
426  if (distance > minimizeDistance) {
427  root.dash();
428  }
429  } else if (root.state === "") {
430  // didn't drag far enough. rollback
431  root.switchToNextState("")
432  }
433  }
434  }
435  }
436 
437  states: [
438  State {
439  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
440  PropertyChanges {
441  target: panel
442  x: -root.panelWidth
443  }
444  },
445  State {
446  name: "visible"
447  PropertyChanges {
448  target: panel
449  x: -root.x // so we never go past panelWidth, even when teased by tutorial
450  }
451  },
452  State {
453  name: "visibleTemporary"
454  extend: "visible"
455  PropertyChanges {
456  target: root
457  autohideEnabled: true
458  }
459  },
460  State {
461  name: "teasing"
462  when: teaseTimer.running && teaseTimer.mode == "teasing"
463  PropertyChanges {
464  target: panel
465  x: -root.panelWidth + units.gu(2)
466  }
467  },
468  State {
469  name: "hinting"
470  when: teaseTimer.running && teaseTimer.mode == "hinting"
471  PropertyChanges {
472  target: panel
473  x: 0
474  }
475  }
476  ]
477 }