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.15
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 function onHint() { hint(); }
308 }
309
310 Connections {
311 target: i18n
312 function 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 Image {
397 anchors.left: drawer.right
398 anchors.top: drawer.top
399 anchors.bottom: drawer.bottom
400 width: units.gu(1)
401 visible: !drawer.fullyClosed
402 source: "../graphics/dropshadow_right@20.png"
403 }
404
405 Drawer {
406 id: drawer
407 objectName: "drawer"
408 anchors {
409 top: parent.top
410 bottom: parent.bottom
411 right: parent.left
412 }
413 background: root.background
414 width: Math.min(root.width, units.gu(81))
415 panelWidth: panel.width
416 allowSlidingAnimation: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate
417
418 onApplicationSelected: {
419 root.launcherApplicationSelected(appId)
420 root.hide();
421 root.focus = false;
422 }
423
424 onHideRequested: {
425 root.hide();
426 }
427
428 onOpenRequested: {
429 root.toggleDrawer(false, true);
430 }
431
432 onFullyClosedChanged: {
433 if (!fullyClosed)
434 return
435
436 drawer.unFocusInput()
437 root.focus = false
438 }
439 }
440
441 LauncherPanel {
442 id: panel
443 objectName: "launcherPanel"
444 enabled: root.available && (root.state == "visible" || root.state == "visibleTemporary" || root.state == "drawer")
445 width: root.panelWidth
446 anchors {
447 top: parent.top
448 bottom: parent.bottom
449 }
450 x: -width
451 visible: root.x > 0 || x > -width || dragArea.pressed || panel.hasPeekingIcon
452 model: LauncherModel
453
454 property var dismissTimer: Timer { interval: 500 }
455 Connections {
456 target: panel.dismissTimer
457 function onTriggered() {
458 if (root.state !== "drawer" && root.autohideEnabled && !root.lockedVisible) {
459 if (!edgeBarrier.containsMouse && !panel.preventHiding) {
460 root.state = ""
461 } else {
462 panel.dismissTimer.restart()
463 }
464 }
465 }
466 }
467
468 property bool animate: true
469
470 onApplicationSelected: {
471 launcherApplicationSelected(appId);
472 root.hide(ignoreHideIfMouseOverLauncher);
473 }
474 onShowDashHome: {
475 root.hide(ignoreHideIfMouseOverLauncher);
476 root.showDashHome();
477 }
478
479 onPreventHidingChanged: {
480 if (panel.dismissTimer.running) {
481 panel.dismissTimer.restart();
482 }
483 }
484
485 onKbdNavigationCancelled: {
486 panel.highlightIndex = -2;
487 root.hide();
488 root.focus = false;
489 }
490
491 onDraggingChanged: {
492 drawer.unFocusInput()
493 }
494
495 Behavior on x {
496 enabled: !dragArea.dragging && !launcherDragArea.drag.active && panel.animate;
497 NumberAnimation {
498 duration: 300
499 easing.type: Easing.OutCubic
500 }
501 }
502
503 Behavior on opacity {
504 NumberAnimation {
505 duration: LomiriAnimation.FastDuration; easing.type: Easing.OutCubic
506 }
507 }
508 }
509
510 EdgeBarrier {
511 id: edgeBarrier
512 edge: Qt.LeftEdge
513 target: parent
514 enabled: root.available
515 onProgressChanged: {
516 if (progress > .5 && root.state != "visibleTemporary" && root.state != "drawer" && root.state != "visible") {
517 root.switchToNextState("visibleTemporary");
518 }
519 }
520 onPassed: {
521 if (root.drawerEnabled) {
522 root.toggleDrawer()
523 }
524 }
525
526 material: Component {
527 Item {
528 Rectangle {
529 width: parent.height
530 height: parent.width
531 rotation: -90
532 anchors.centerIn: parent
533 gradient: Gradient {
534 GradientStop { position: 0.0; color: Qt.rgba(panel.color.r, panel.color.g, panel.color.b, .5)}
535 GradientStop { position: 1.0; color: Qt.rgba(panel.color.r,panel.color.g,panel.color.b,0)}
536 }
537 }
538 }
539 }
540 }
541
542 SwipeArea {
543 id: dragArea
544 objectName: "launcherDragArea"
545
546 direction: Direction.Rightwards
547
548 enabled: root.available
549 x: -root.x // so if launcher is adjusted relative to screen, we stay put (like tutorial does when teasing)
550 width: root.dragAreaWidth
551 height: root.height
552
553 function easeInOutCubic(t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }
554
555 property var lastDragPoints: []
556
557 function dragDirection() {
558 if (lastDragPoints.length < 5) {
559 return "unknown";
560 }
561
562 var toRight = true;
563 var toLeft = true;
564 for (var i = lastDragPoints.length - 5; i < lastDragPoints.length; i++) {
565 if (toRight && lastDragPoints[i] < lastDragPoints[i-1]) {
566 toRight = false;
567 }
568 if (toLeft && lastDragPoints[i] > lastDragPoints[i-1]) {
569 toLeft = false;
570 }
571 }
572 return toRight ? "right" : toLeft ? "left" : "unknown";
573 }
574
575 onDistanceChanged: {
576 if (dragging && launcher.state != "visible" && launcher.state != "drawer") {
577 panel.x = -panel.width + Math.min(Math.max(0, distance), panel.width);
578 }
579
580 if (root.drawerEnabled && dragging && launcher.state != "drawer") {
581 lastDragPoints.push(distance)
582 var drawerHintDistance = panel.width + units.gu(1)
583 if (distance < drawerHintDistance) {
584 drawer.anchors.rightMargin = -Math.min(Math.max(0, distance), drawer.width);
585 } else {
586 var linearDrawerX = Math.min(Math.max(0, distance - drawerHintDistance), drawer.width);
587 var linearDrawerProgress = linearDrawerX / (drawer.width)
588 var easedDrawerProgress = easeInOutCubic(linearDrawerProgress);
589 drawer.anchors.rightMargin = -(drawerHintDistance + easedDrawerProgress * (drawer.width - drawerHintDistance));
590 }
591 }
592 }
593
594 onDraggingChanged: {
595 if (!dragging) {
596 if (distance > panel.width / 2) {
597 if (root.drawerEnabled && distance > panel.width * 3 && dragDirection() !== "left") {
598 root.toggleDrawer(false)
599 } else {
600 root.switchToNextState("visible");
601 }
602 } else if (root.state === "") {
603 // didn't drag far enough. rollback
604 root.switchToNextState("");
605 }
606 }
607 lastDragPoints = [];
608 }
609
610 GestureAreaSizeHint {
611 anchors.fill: parent
612 }
613 }
614
615 states: [
616 State {
617 name: "" // hidden state. Must be the default state ("") because "when:" falls back to this.
618 PropertyChanges {
619 target: panel
620 restoreEntryValues: false
621 x: -root.panelWidth
622 }
623 PropertyChanges {
624 target: drawer
625 restoreEntryValues: false
626 anchors.rightMargin: 0
627 focus: false
628 }
629 },
630 State {
631 name: "visible"
632 PropertyChanges {
633 target: panel
634 restoreEntryValues: false
635 x: -root.x // so we never go past panelWidth, even when teased by tutorial
636 focus: true
637 }
638 PropertyChanges {
639 target: drawer
640 restoreEntryValues: false
641 anchors.rightMargin: 0
642 focus: false
643 }
644 PropertyChanges {
645 target: root
646 restoreEntryValues: false
647 autohideEnabled: false
648 }
649 },
650 State {
651 name: "drawer"
652 PropertyChanges {
653 target: panel
654 restoreEntryValues: false
655 x: -root.x // so we never go past panelWidth, even when teased by tutorial
656 focus: false
657 }
658 PropertyChanges {
659 target: drawer
660 restoreEntryValues: false
661 anchors.rightMargin: -drawer.width + root.x // so we never go past panelWidth, even when teased by tutorial
662 focus: true
663 }
664 },
665 State {
666 name: "visibleTemporary"
667 extend: "visible"
668 PropertyChanges {
669 target: root
670 restoreEntryValues: false
671 autohideEnabled: true
672 }
673 },
674 State {
675 name: "teasing"
676 when: teaseTimer.running && teaseTimer.mode == "teasing"
677 PropertyChanges {
678 target: panel
679 restoreEntryValues: false
680 x: -root.panelWidth + units.gu(2)
681 }
682 },
683 State {
684 name: "hinting"
685 when: teaseTimer.running && teaseTimer.mode == "hinting"
686 PropertyChanges {
687 target: panel
688 restoreEntryValues: false
689 x: 0
690 }
691 }
692 ]
693}