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  property bool shadeBackground: true // can be used to disable background shade when launcher is visible
32 
33  property int panelWidth: units.gu(10)
34  property int dragAreaWidth: units.gu(1)
35  property int minimizeDistance: units.gu(26)
36  property real progress: dragArea.dragging && dragArea.touchX > panelWidth ?
37  (width * (dragArea.touchX-panelWidth) / (width - panelWidth)) : 0
38 
39  property bool superPressed: false
40  property bool superTabPressed: false
41 
42  readonly property bool dragging: dragArea.dragging
43  readonly property real dragDistance: dragArea.dragging ? dragArea.touchX : 0
44  readonly property real visibleWidth: panel.width + panel.x
45 
46  readonly property bool shown: panel.x > -panel.width
47 
48  // emitted when an application is selected
49  signal launcherApplicationSelected(string appId)
50 
51  // emitted when the apps dash should be shown because of a swipe gesture
52  signal dash()
53 
54  // emitted when the dash icon in the launcher has been tapped
55  signal showDashHome()
56 
57  onStateChanged: {
58  if (state == "") {
59  panel.dismissTimer.stop()
60  } else {
61  panel.dismissTimer.restart()
62  }
63  }
64 
65  onSuperPressedChanged: {
66  if (superPressed) {
67  superPressTimer.start();
68  superLongPressTimer.start();
69  } else {
70  superPressTimer.stop();
71  superLongPressTimer.stop();
72  launcher.switchToNextState("");
73  panel.shortcutHintsShown = false;
74  }
75  }
76 
77  onSuperTabPressedChanged: {
78  if (superTabPressed) {
79  switchToNextState("visible")
80  panel.highlightIndex = -1;
81  root.focus = true;
82  superPressTimer.stop();
83  superLongPressTimer.stop();
84  } else {
85  if (panel.highlightIndex == -1) {
86  showDashHome();
87  } else if (panel.highlightIndex >= 0){
88  launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
89  }
90  panel.highlightIndex = -2;
91  switchToNextState("");
92  root.focus = false;
93  }
94  }
95 
96  onLockedVisibleChanged: {
97  if (lockedVisible && state == "") {
98  panel.dismissTimer.stop();
99  fadeOutAnimation.stop();
100  switchToNextState("visible")
101  } else if (!lockedVisible && state == "visible") {
102  hide();
103  }
104  }
105 
106  function hide() {
107  switchToNextState("")
108  }
109 
110  function fadeOut() {
111  if (!root.lockedVisible) {
112  fadeOutAnimation.start();
113  }
114  }
115 
116  function switchToNextState(state) {
117  animateTimer.nextState = state
118  animateTimer.start();
119  }
120 
121  function tease() {
122  if (available && !dragArea.dragging) {
123  teaseTimer.mode = "teasing"
124  teaseTimer.start();
125  }
126  }
127 
128  function hint() {
129  if (available && root.state == "") {
130  teaseTimer.mode = "hinting"
131  teaseTimer.start();
132  }
133  }
134 
135  function pushEdge(amount) {
136  if (root.state === "") {
137  edgeBarrier.push(amount);
138  }
139  }
140 
141  function openForKeyboardNavigation() {
142  panel.highlightIndex = -1; // The BFB
143  root.focus = true;
144  switchToNextState("visible")
145  }
146 
147  Keys.onPressed: {
148  switch (event.key) {
149  case Qt.Key_Backtab:
150  panel.highlightPrevious();
151  event.accepted = true;
152  break;
153  case Qt.Key_Up:
154  if (root.inverted) {
155  panel.highlightNext()
156  } else {
157  panel.highlightPrevious();
158  }
159  event.accepted = true;
160  break;
161  case Qt.Key_Tab:
162  panel.highlightNext();
163  event.accepted = true;
164  break;
165  case Qt.Key_Down:
166  if (root.inverted) {
167  panel.highlightPrevious();
168  } else {
169  panel.highlightNext();
170  }
171  event.accepted = true;
172  break;
173  case Qt.Key_Right:
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  MouseArea {
279  id: launcherDragArea
280  enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
281  anchors.fill: panel
282  anchors.rightMargin: -units.gu(2)
283  drag {
284  axis: Drag.XAxis
285  maximumX: 0
286  target: panel
287  }
288 
289  onReleased: {
290  if (panel.x < -panel.width/3) {
291  root.switchToNextState("")
292  } else {
293  root.switchToNextState("visible")
294  }
295  }
296  }
297 
298  InverseMouseArea {
299  id: closeMouseArea
300  anchors.fill: panel
301  enabled: root.shadeBackground && root.state == "visible" && (!root.lockedVisible || panel.highlightIndex >= -1)
302  visible: enabled
303  onPressed: {
304  panel.highlightIndex = -2
305  root.hide();
306  }
307  }
308 
309  Rectangle {
310  id: backgroundShade
311  anchors.fill: parent
312  color: "black"
313  opacity: root.shadeBackground && root.state == "visible" && !root.lockedVisible ? 0.6 : 0
314 
315  Behavior on opacity { NumberAnimation { duration: UbuntuAnimation.BriskDuration } }
316  }
317 
318  EdgeBarrier {
319  id: edgeBarrier
320  edge: Qt.LeftEdge
321  target: parent
322  enabled: root.available
323  onPassed: { root.switchToNextState("visibleTemporary"); }
324  material: Component {
325  Item {
326  Rectangle {
327  width: parent.height
328  height: parent.width
329  rotation: -90
330  anchors.centerIn: parent
331  gradient: Gradient {
332  GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
333  GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
334  }
335  }
336  }
337  }
338  }
339 
340  LauncherPanel {
341  id: panel
342  objectName: "launcherPanel"
343  enabled: root.available && root.state == "visible" || root.state == "visibleTemporary"
344  width: root.panelWidth
345  anchors {
346  top: parent.top
347  bottom: parent.bottom
348  }
349  x: -width
350  visible: root.x > 0 || x > -width || dragArea.pressed
351  model: LauncherModel
352 
353  property var dismissTimer: Timer { interval: 500 }
354  Connections {
355  target: panel.dismissTimer
356  onTriggered: {
357  if (root.autohideEnabled && !root.lockedVisible) {
358  if (!panel.preventHiding) {
359  root.state = ""
360  } else {
361  panel.dismissTimer.restart()
362  }
363  }
364  }
365  }
366 
367  property bool animate: true
368 
369  onApplicationSelected: {
370  root.hide();
371  launcherApplicationSelected(appId)
372  }
373  onShowDashHome: {
374  root.hide();
375  root.showDashHome();
376  }
377 
378  onPreventHidingChanged: {
379  if (panel.dismissTimer.running) {
380  panel.dismissTimer.restart();
381  }
382  }
383 
384  onKbdNavigationCancelled: {
385  panel.highlightIndex = -2;
386  root.hide();
387  root.focus = false;
388  }
389 
390  Behavior on x {
391  enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
392  NumberAnimation {
393  duration: 300
394  easing.type: Easing.OutCubic
395  }
396  }
397 
398  Behavior on opacity {
399  NumberAnimation {
400  duration: UbuntuAnimation.FastDuration; easing.type: Easing.OutCubic
401  }
402  }
403  }
404 
405  DirectionalDragArea {
406  id: dragArea
407  objectName: "launcherDragArea"
408 
409  direction: Direction.Rightwards
410 
411  enabled: root.available
412  x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
413  width: root.dragAreaWidth
414  height: root.height
415 
416  onDistanceChanged: {
417  if (!dragging || launcher.state == "visible")
418  return;
419 
420  panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
421  }
422 
423  onDraggingChanged: {
424  if (!dragging) {
425  if (distance > panel.width / 2) {
426  root.switchToNextState("visible")
427  if (distance > minimizeDistance) {
428  root.dash();
429  }
430  } else if (root.state === "") {
431  // didn't drag far enough. rollback
432  root.switchToNextState("")
433  }
434  }
435  }
436  }
437 
438  states: [
439  State {
440  name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
441  PropertyChanges {
442  target: panel
443  x: -root.panelWidth
444  }
445  },
446  State {
447  name: "visible"
448  PropertyChanges {
449  target: panel
450  x: -root.x // so we never go past panelWidth, even when teased by tutorial
451  }
452  },
453  State {
454  name: "visibleTemporary"
455  extend: "visible"
456  PropertyChanges {
457  target: root
458  autohideEnabled: true
459  }
460  },
461  State {
462  name: "teasing"
463  when: teaseTimer.running && teaseTimer.mode == "teasing"
464  PropertyChanges {
465  target: panel
466  x: -root.panelWidth + units.gu(2)
467  }
468  },
469  State {
470  name: "hinting"
471  when: teaseTimer.running && teaseTimer.mode == "hinting"
472  PropertyChanges {
473  target: panel
474  x: 0
475  }
476  }
477  ]
478 }