Lomiri
Loading...
Searching...
No Matches
Launcher.qml
1/*
2 * Copyright (C) 2013-2015 Canonical Ltd.
3 * Copyright (C) 2021 UBports Foundation
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18import QtQuick 2.12
19import "../Components"
20import Lomiri.Components 1.3
21import Lomiri.Gestures 0.1
22import Lomiri.Launcher 0.1
23import Utils 0.1 as Utils
24
25FocusScope {
26 id: root
27
28 readonly property int ignoreHideIfMouseOverLauncher: 1
29
30 property bool autohideEnabled: false
31 property bool lockedVisible: false
32 property bool available: true // can be used to disable all interactions
33 property alias inverted: panel.inverted
34 property Item blurSource: null
35 property int topPanelHeight: 0
36 property bool drawerEnabled: true
37 property alias privateMode: panel.privateMode
38 property url background
39
40 property int panelWidth: units.gu(10)
41 property int dragAreaWidth: units.gu(1)
42 property real progress: dragArea.dragging && dragArea.touchPosition.x > panelWidth ?
43 (width * (dragArea.touchPosition.x-panelWidth) / (width - panelWidth)) : 0
44
45 property bool superPressed: false
46 property bool superTabPressed: false
47 property bool takesFocus: false;
48
49 readonly property bool dragging: dragArea.dragging
50 readonly property real dragDistance: dragArea.dragging ? dragArea.touchPosition.x : 0
51 readonly property real visibleWidth: panel.width + panel.x
52 readonly property alias shortcutHintsShown: panel.shortcutHintsShown
53
54 readonly property bool shown: panel.x > -panel.width
55 readonly property bool drawerShown: drawer.x == 0
56
57 // emitted when an application is selected
58 signal launcherApplicationSelected(string appId)
59
60 // emitted when the dash icon in the launcher has been tapped
61 signal showDashHome()
62
63 onStateChanged: {
64 if (state == "") {
65 panel.dismissTimer.stop()
66 } else {
67 panel.dismissTimer.restart()
68 }
69 }
70
71 onFocusChanged: {if (!focus) { root.takesFocus = false; }}
72
73 onSuperPressedChanged: {
74 if (state == "drawer")
75 return;
76
77 if (superPressed) {
78 superPressTimer.start();
79 superLongPressTimer.start();
80 } else {
81 superPressTimer.stop();
82 superLongPressTimer.stop();
83 switchToNextState(root.lockedVisible ? "visible" : "");
84 panel.shortcutHintsShown = false;
85 }
86 }
87
88 onSuperTabPressedChanged: {
89 if (superTabPressed) {
90 switchToNextState("visible")
91 panel.highlightIndex = -1;
92 root.takesFocus = true;
93 root.focus = true;
94 superPressTimer.stop();
95 superLongPressTimer.stop();
96 } else {
97 switchToNextState(root.lockedVisible ? "visible" : "");
98 root.focus = false;
99 if (panel.highlightIndex == -1) {
100 root.showDashHome();
101 } else if (panel.highlightIndex >= 0){
102 launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
103 }
104 panel.highlightIndex = -2;
105 }
106 }
107
108 onLockedVisibleChanged: {
109 // We are in the progress of moving to the drawer
110 // this is caused by the user pressing the bfb on unlock
111 // in this case we want to show the drawer and not
112 // just visible
113 if (animateTimer.nextState == "drawer")
114 return;
115
116 if (lockedVisible && state == "") {
117 panel.dismissTimer.stop();
118 fadeOutAnimation.stop();
119 switchToNextState("visible")
120 } else if (!lockedVisible && (state == "visible" || state == "drawer")) {
121 hide();
122 }
123 }
124
125 onPanelWidthChanged: {
126 hint();
127 }
128
129 // Switches the Launcher to the visible state, but only if it's not already
130 // opened.
131 // Prevents closing the Drawer when trying to show the Launcher.
132 function show() {
133 if (state === "" || state === "visibleTemporary") {
134 switchToNextState("visible");
135 }
136 }
137
138 function hide(flags) {
139 if ((flags & ignoreHideIfMouseOverLauncher) && Utils.Functions.itemUnderMouse(panel)) {
140 if (state == "drawer") {
141 switchToNextState("visibleTemporary");
142 }
143 return;
144 }
145 if (root.lockedVisible) {
146 // Due to binding updates when switching between modes
147 // it could happen that our request to show will be overwritten
148 // with a hide request. Rewrite it when we know hiding is not allowed.
149 switchToNextState("visible")
150 } else {
151 switchToNextState("")
152 }
153 root.focus = false;
154 }
155
156 function fadeOut() {
157 if (!root.lockedVisible) {
158 fadeOutAnimation.start();
159 }
160 }
161
162 function switchToNextState(state) {
163 animateTimer.nextState = state
164 animateTimer.start();
165 }
166
167 function tease() {
168 if (available && !dragArea.dragging) {
169 teaseTimer.mode = "teasing"
170 teaseTimer.start();
171 }
172 }
173
174 function hint() {
175 if (available && root.state == "") {
176 teaseTimer.mode = "hinting"
177 teaseTimer.start();
178 }
179 }
180
181 function pushEdge(amount) {
182 if (root.state === "" || root.state == "visible" || root.state == "visibleTemporary") {
183 edgeBarrier.push(amount);
184 }
185 }
186
187 function openForKeyboardNavigation() {
188 panel.highlightIndex = -1; // The BFB
189 drawer.focus = false;
190 root.takesFocus = true;
191 root.focus = true;
192 switchToNextState("visible")
193 }
194
195 function toggleDrawer(focusInputField, onlyOpen, alsoToggleLauncher) {
196 if (!drawerEnabled) {
197 return;
198 }
199
200 panel.shortcutHintsShown = false;
201 superPressTimer.stop();
202 superLongPressTimer.stop();
203 root.takesFocus = true;
204 root.focus = true;
205 if (focusInputField) {
206 drawer.focusInput();
207 }
208 if (state === "drawer" && !onlyOpen)
209 if (alsoToggleLauncher && !root.lockedVisible)
210 switchToNextState("");
211 else
212 switchToNextState("visible");
213 else
214 switchToNextState("drawer");
215 }
216
217 Keys.onPressed: {
218 switch (event.key) {
219 case Qt.Key_Backtab:
220 panel.highlightPrevious();
221 event.accepted = true;
222 break;
223 case Qt.Key_Up:
224 if (root.inverted) {
225 panel.highlightNext()
226 } else {
227 panel.highlightPrevious();
228 }
229 event.accepted = true;
230 break;
231 case Qt.Key_Tab:
232 panel.highlightNext();
233 event.accepted = true;
234 break;
235 case Qt.Key_Down:
236 if (root.inverted) {
237 panel.highlightPrevious();
238 } else {
239 panel.highlightNext();
240 }
241 event.accepted = true;
242 break;
243 case Qt.Key_Right:
244 case Qt.Key_Menu:
245 panel.openQuicklist(panel.highlightIndex)
246 event.accepted = true;
247 break;
248 case Qt.Key_Escape:
249 panel.highlightIndex = -2;
250 // Falling through intentionally
251 case Qt.Key_Enter:
252 case Qt.Key_Return:
253 case Qt.Key_Space:
254 if (panel.highlightIndex == -1) {
255 root.showDashHome();
256 } else if (panel.highlightIndex >= 0) {
257 launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
258 }
259 root.hide();
260 panel.highlightIndex = -2
261 event.accepted = true;
262 }
263 }
264
265 Timer {
266 id: superPressTimer
267 interval: 200
268 onTriggered: {
269 switchToNextState("visible")
270 }
271 }
272
273 Timer {
274 id: superLongPressTimer
275 interval: 1000
276 onTriggered: {
277 switchToNextState("visible")
278 panel.shortcutHintsShown = true;
279 }
280 }
281
282 Timer {
283 id: teaseTimer
284 interval: mode == "teasing" ? 200 : 300
285 property string mode: "teasing"
286 }
287
288 // Because the animation on x is disabled while dragging
289 // switching state directly in the drag handlers would not animate
290 // the completion of the hide/reveal gesture. Lets update the state
291 // machine and switch to the final state in the next event loop run
292 Timer {
293 id: animateTimer
294 objectName: "animateTimer"
295 interval: 1
296 property string nextState: ""
297 onTriggered: {
298 // switching to an intermediate state here to make sure all the
299 // values are restored, even if we were already in the target state
300 root.state = "tmp"
301 root.state = nextState
302 }
303 }
304
305 Connections {
306 target: LauncherModel
307 onHint: hint();
308 }
309
310 Connections {
311 target: i18n
312 onLanguageChanged: LauncherModel.refresh()
313 }
314
315 SequentialAnimation {
316 id: fadeOutAnimation
317 ScriptAction {
318 script: {
319 animateTimer.stop(); // Don't change the state behind our back
320 panel.layer.enabled = true
321 }
322 }
323 LomiriNumberAnimation {
324 target: panel
325 property: "opacity"
326 easing.type: Easing.InQuad
327 to: 0
328 }
329 ScriptAction {
330 script: {
331 panel.layer.enabled = false
332 panel.animate = false;
333 root.state = "";
334 panel.x = -panel.width
335 panel.opacity = 1;
336 panel.animate = true;
337 }
338 }
339 }
340
341 InverseMouseArea {
342 id: closeMouseArea
343 anchors.fill: panel
344 enabled: (root.state == "visible" && !root.lockedVisible) || root.state == "drawer" || hoverEnabled
345 hoverEnabled: panel.quickListOpen
346 visible: enabled
347 onPressed: {
348 mouse.accepted = false;
349 panel.highlightIndex = -2;
350 root.hide();
351 }
352 }
353
354 MouseArea {
355 id: launcherDragArea
356 enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary") && !root.lockedVisible
357 anchors.fill: panel
358 anchors.rightMargin: -units.gu(2)
359 drag {
360 axis: Drag.XAxis
361 maximumX: 0
362 target: panel
363 }
364
365 onReleased: {
366 if (panel.x < -panel.width/3) {
367 root.switchToNextState("")
368 } else {
369 root.switchToNextState("visible")
370 }
371 }
372 }
373
374 Item {
375 clip: true
376 x: 0
377 y: drawer.y
378 width: drawer.width + drawer.x
379 height: drawer.height
380 BackgroundBlur {
381 id: backgroundBlur
382 x: 0
383 y: 0
384 width: drawer.width
385 height: drawer.height
386 visible: drawer.x > -drawer.width
387 sourceItem: root.blurSource
388 blurRect: Qt.rect(0,
389 root.topPanelHeight,
390 drawer.width,
391 drawer.height)
392 occluding: (drawer.width == root.width) && drawer.fullyOpen
393 }
394 }
395
396 Drawer {
397 id: drawer
398 objectName: "drawer"
399 anchors {
400 top: parent.top
401 topMargin: root.inverted ? root.topPanelHeight : 0
402 bottom: parent.bottom
403 right: parent.left
404 }
405 background: root.background
406 width: Math.min(root.width, units.gu(81))
407 panelWidth: panel.width
408 allowSlidingAnimation: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate
409
410 onApplicationSelected: {
411 root.launcherApplicationSelected(appId)
412 root.hide();
413 root.focus = false;
414 }
415
416 onHideRequested: {
417 root.hide();
418 }
419
420 onOpenRequested: {
421 root.toggleDrawer(false, true);
422 }
423
424 onFullyClosedChanged: {
425 if (!fullyClosed)
426 return
427
428 drawer.unFocusInput()
429 root.focus = false
430 }
431 }
432
433 LauncherPanel {
434 id: panel
435 objectName: "launcherPanel"
436 enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary" || root.state == "drawer")
437 width: root.panelWidth
438 anchors {
439 top: parent.top
440 bottom: parent.bottom
441 }
442 x: -width
443 visible: root.x > 0 || x > -width || dragArea.pressed
444 model: LauncherModel
445
446 property var dismissTimer: Timer { interval: 500 }
447 Connections {
448 target: panel.dismissTimer
449 onTriggered: {
450 if (root.state !== "drawer" && root.autohideEnabled && !root.lockedVisible) {
451 if (!edgeBarrier.containsMouse && !panel.preventHiding) {
452 root.state = ""
453 } else {
454 panel.dismissTimer.restart()
455 }
456 }
457 }
458 }
459
460 property bool animate: true
461
462 onApplicationSelected: {
463 launcherApplicationSelected(appId);
464 root.hide(ignoreHideIfMouseOverLauncher);
465 }
466 onShowDashHome: {
467 root.hide(ignoreHideIfMouseOverLauncher);
468 root.showDashHome();
469 }
470
471 onPreventHidingChanged: {
472 if (panel.dismissTimer.running) {
473 panel.dismissTimer.restart();
474 }
475 }
476
477 onKbdNavigationCancelled: {
478 panel.highlightIndex = -2;
479 root.hide();
480 root.focus = false;
481 }
482
483 onDraggingChanged: {
484 drawer.unFocusInput()
485 }
486
487 Behavior on x {
488 enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
489 NumberAnimation {
490 duration: 300
491 easing.type: Easing.OutCubic
492 }
493 }
494
495 Behavior on opacity {
496 NumberAnimation {
497 duration: LomiriAnimation.FastDuration; easing.type: Easing.OutCubic
498 }
499 }
500 }
501
502 EdgeBarrier {
503 id: edgeBarrier
504 edge: Qt.LeftEdge
505 target: parent
506 enabled: root.available
507 onProgressChanged: {
508 if (progress > .5 && root.state != "visibleTemporary" && root.state != "drawer" && root.state != "visible") {
509 root.switchToNextState("visibleTemporary");
510 }
511 }
512 onPassed: {
513 if (root.drawerEnabled) {
514 root.toggleDrawer()
515 }
516 }
517
518 material: Component {
519 Item {
520 Rectangle {
521 width: parent.height
522 height: parent.width
523 rotation: -90
524 anchors.centerIn: parent
525 gradient: Gradient {
526 GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
527 GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
528 }
529 }
530 }
531 }
532 }
533
534 SwipeArea {
535 id: dragArea
536 objectName: "launcherDragArea"
537
538 direction: Direction.Rightwards
539
540 enabled: root.available
541 x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
542 width: root.dragAreaWidth
543 height: root.height
544
545 function easeInOutCubic(t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }
546
547 property var lastDragPoints: []
548
549 function dragDirection() {
550 if (lastDragPoints.length < 5) {
551 return "unknown";
552 }
553
554 var toRight = true;
555 var toLeft = true;
556 for (var i = lastDragPoints.length - 5; i < lastDragPoints.length; i++) {
557 if (toRight && lastDragPoints[i] < lastDragPoints[i-1]) {
558 toRight = false;
559 }
560 if (toLeft && lastDragPoints[i] > lastDragPoints[i-1]) {
561 toLeft = false;
562 }
563 }
564 return toRight ? "right" : toLeft ? "left" : "unknown";
565 }
566
567 onDistanceChanged: {
568 if (dragging && launcher.state != "visible" && launcher.state != "drawer") {
569 panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
570 }
571
572 if (root.drawerEnabled && dragging && launcher.state != "drawer") {
573 lastDragPoints.push(distance)
574 var drawerHintDistance = panel.width + units.gu(1)
575 if (distance < drawerHintDistance) {
576 drawer.anchors.rightMargin = -Math.min(Math.max(0, distance), drawer.width);
577 } else {
578 var linearDrawerX = Math.min(Math.max(0, distance - drawerHintDistance), drawer.width);
579 var linearDrawerProgress = linearDrawerX / (drawer.width)
580 var easedDrawerProgress = easeInOutCubic(linearDrawerProgress);
581 drawer.anchors.rightMargin = -(drawerHintDistance + easedDrawerProgress * (drawer.width - drawerHintDistance));
582 }
583 }
584 }
585
586 onDraggingChanged: {
587 if (!dragging) {
588 if (distance > panel.width / 2) {
589 if (root.drawerEnabled && distance > panel.width * 3 && dragDirection() !== "left") {
590 root.toggleDrawer(false)
591 } else {
592 root.switchToNextState("visible");
593 }
594 } else if (root.state === "") {
595 // didn't drag far enough. rollback
596 root.switchToNextState("");
597 }
598 }
599 lastDragPoints = [];
600 }
601 }
602
603 states: [
604 State {
605 name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
606 PropertyChanges {
607 target: panel
608 restoreEntryValues: false
609 x: -root.panelWidth
610 }
611 PropertyChanges {
612 target: drawer
613 restoreEntryValues: false
614 anchors.rightMargin: 0
615 focus: false
616 }
617 },
618 State {
619 name: "visible"
620 PropertyChanges {
621 target: panel
622 restoreEntryValues: false
623 x: -root.x // so we never go past panelWidth, even when teased by tutorial
624 focus: true
625 }
626 PropertyChanges {
627 target: drawer
628 restoreEntryValues: false
629 anchors.rightMargin: 0
630 focus: false
631 }
632 PropertyChanges {
633 target: root
634 restoreEntryValues: false
635 autohideEnabled: false
636 }
637 },
638 State {
639 name: "drawer"
640 PropertyChanges {
641 target: panel
642 restoreEntryValues: false
643 x: -root.x // so we never go past panelWidth, even when teased by tutorial
644 focus: false
645 }
646 PropertyChanges {
647 target: drawer
648 restoreEntryValues: false
649 anchors.rightMargin: -drawer.width + root.x // so we never go past panelWidth, even when teased by tutorial
650 focus: true
651 }
652 },
653 State {
654 name: "visibleTemporary"
655 extend: "visible"
656 PropertyChanges {
657 target: root
658 restoreEntryValues: false
659 autohideEnabled: true
660 }
661 },
662 State {
663 name: "teasing"
664 when: teaseTimer.running && teaseTimer.mode == "teasing"
665 PropertyChanges {
666 target: panel
667 restoreEntryValues: false
668 x: -root.panelWidth + units.gu(2)
669 }
670 },
671 State {
672 name: "hinting"
673 when: teaseTimer.running && teaseTimer.mode == "hinting"
674 PropertyChanges {
675 target: panel
676 restoreEntryValues: false
677 x: 0
678 }
679 }
680 ]
681}