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