shift-shell/components/mobileshell/qml/actiondrawer/ActionDrawer.qml

335 lines
10 KiB
QML
Raw Normal View History

/*
* SPDX-FileCopyrightText: 2014 Marco Martin <notmart@gmail.com>
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
Item {
id: root
2024-07-27 03:47:44 +00:00
/*
* The intended visiblity of the action drawer.
*
* This is separate from "visible" in order to avoid having to set
* item visiblity when its on its own window (wasteful since the window itself can be shown/hidden).
*/
property bool intendedToBeVisible: false
/**
* The model for the notification widget.
*/
property var notificationModel
2024-07-27 03:47:44 +00:00
/**
* The model type for the notification widget.
*/
property var notificationModelType: MobileShell.NotificationsModelType.NotificationsModel
2024-07-27 03:47:44 +00:00
/**
* The model for the quick settings.
*/
property QS.QuickSettingsModel quickSettingsModel: QS.QuickSettingsModel {}
/**
* The notification settings object to be used in the notification widget.
*/
property var notificationSettings
/**
* Whether actions should be subject to restricted permissions (ex. lockscreen).
2024-07-27 03:47:44 +00:00
*
* The permissionsRequested() signal emits when authentication is requested.
*/
property bool restrictedPermissions: false
2024-07-27 03:47:44 +00:00
/**
* The amount of pixels moved by touch/mouse in the process of opening/closing the panel.
*/
property real offset: 0
2024-07-27 03:47:44 +00:00
/**
* Whether the panel is being dragged.
*/
property bool dragging: false
2024-07-27 03:47:44 +00:00
/**
* Whether the panel is open after touch/mouse release from the first opening swipe.
*/
property bool opened: false
/**
2022-05-25 16:13:29 +00:00
* Whether the panel should open to pinned mode first, with a second stroke needed to full open.
* Only applies to portrait mode.
*/
2022-05-25 16:13:29 +00:00
property bool openToPinnedMode: true
2024-07-27 03:47:44 +00:00
/**
* Direction the panel is currently moving in.
*/
property int direction: MobileShell.Direction.None
2024-07-27 03:47:44 +00:00
/**
* The notifications widget being shown. May be null.
*/
property var notificationsWidget: contentContainer.notificationsWidget
2024-07-27 03:47:44 +00:00
/**
* The mode of the action drawer (portrait or landscape).
*/
property int mode: (height > width && width <= largePortraitThreshold) ? ActionDrawer.Portrait : ActionDrawer.Landscape
2024-07-27 03:47:44 +00:00
/**
* At some point, even if the screen is technically portrait, if we have a ton of width it'd be best to just show the landscape mode.
*/
readonly property real largePortraitThreshold: Kirigami.Units.gridUnit * 35
2024-07-27 03:47:44 +00:00
enum Mode {
Portrait = 0,
Landscape
}
2024-07-27 03:47:44 +00:00
/**
* Emitted when the drawer has closed.
*/
signal drawerClosed()
2024-07-27 03:47:44 +00:00
/**
* Emitted when the drawer has opened.
*/
signal drawerOpened()
2024-07-27 03:47:44 +00:00
/**
* Emitted when permissions are requested (ex. unlocking the phone).
2024-07-27 03:47:44 +00:00
*
* Only gets emitted when restrictedPermissions is set to true.
*/
signal permissionsRequested()
2024-07-27 03:47:44 +00:00
/**
* Runs the held notification action that was pending for authentication.
2024-07-27 03:47:44 +00:00
*
* Should be called by users if authentication is successful after permissionsRequested() was emitted.
*/
signal runPendingNotificationAction()
onOpenedChanged: {
if (opened) swipeArea.focus = true;
}
property real oldOffset
onOffsetChanged: {
if (offset < 0) {
offset = 0;
}
2024-07-27 03:47:44 +00:00
root.direction = (oldOffset === offset)
? MobileShell.Direction.None
: (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up);
2024-07-27 03:47:44 +00:00
oldOffset = offset;
2024-07-27 03:47:44 +00:00
// close panel immediately after panel is not shown, and the flickable is not being dragged
if (opened && root.offset <= 0 && !swipeArea.moving && !drawerAnimation.running) {
root.state = "";
offset = 0;
focus = false;
root.opened = false;
root.updateState();
}
}
function cancelAnimations() {
root.state = "";
}
2024-07-27 03:47:44 +00:00
function open() {
cancelAnimations();
if (openToPinnedMode) {
root.state = "open"; // go to pinned height
} else {
root.state = "expand"; // go to maximized height
}
}
2024-07-27 03:47:44 +00:00
function closeImmediately() {
cancelAnimations();
offset = 0;
root.state = "close";
}
2024-07-27 03:47:44 +00:00
function close() {
cancelAnimations();
root.state = "close";
}
2024-07-27 03:47:44 +00:00
function expand() {
cancelAnimations();
root.state = "expand";
}
2024-07-27 03:47:44 +00:00
function updateState() {
let openThreshold = Kirigami.Units.gridUnit;
2024-07-27 03:47:44 +00:00
if (root.offset <= 0) {
2024-07-27 03:47:44 +00:00
// close immediately, so that we don't have to wait Kirigami.Units.longDuration
root.intendedToBeVisible = false;
close();
} else if (root.direction === MobileShell.Direction.None || !root.opened) {
2024-07-27 03:47:44 +00:00
// if the panel has not been opened yet, run open animation only if drag passed threshold
(root.offset < openThreshold) ? close() : open();
2024-07-27 03:47:44 +00:00
} else if (root.offset > contentContainer.maximizedQuickSettingsOffset) {
// if drag has gone past the fully expanded view
expand();
} else if (root.offset > contentContainer.minimizedQuickSettingsOffset) {
// if drag is between pinned view and fully expanded view
if (root.direction === MobileShell.Direction.Down) {
expand();
} else {
// go back to pinned, or close if pinned mode is disabled
openToPinnedMode ? open() : close();
}
} else if (root.direction === MobileShell.Direction.Down) {
// if drag is between pinned view and open view, and dragging down
open();
} else {
// if drag is between pinned view and open view, and dragging up
close();
}
}
Timer {
id: updateStateTimer
interval: 0
onTriggered: updateState()
}
state: "close"
states: [
State {
name: ""
PropertyChanges {
target: root; offset: offset
}
},
State {
name: "close"
PropertyChanges {
target: root; offset: 0
}
},
State {
name: "open"
PropertyChanges {
target: root; offset: contentContainer.minimizedQuickSettingsOffset
}
},
State {
name: "expand"
PropertyChanges {
target: root; offset: contentContainer.maximizedQuickSettingsOffset
}
}
]
transitions: Transition {
SequentialAnimation {
PropertyAnimation {
id: drawerAnimation
properties: "offset"; easing.type: Easing.OutExpo; duration: root.state != "" ? Kirigami.Units.veryLongDuration : 0
}
ScriptAction {
script: {
if (root.state != "") {
if (root.offset <= 0) {
root.intendedToBeVisible = false;
root.opened = false;
root.state = "";
} else {
root.opened = true;
}
}
}
}
}
}
2024-07-27 03:47:44 +00:00
MobileShell.SwipeArea {
id: swipeArea
2023-09-30 17:06:41 +00:00
mode: MobileShell.SwipeArea.VerticalOnly
anchors.fill: parent
function startSwipeWithPoint(point) {
root.cancelAnimations();
root.dragging = true;
// Immediately open action drawer if we interact with it and it's already open
// This allows us to have 2 quick flicks from minimized -> expanded
if (root.intendedToBeVisible && !root.opened) {
root.opened = true;
}
// if the user swiped from the top left, otherwise it's from the top right
if (!root.intendedToBeVisible) {
if (point.x < root.width / 2) {
root.openToPinnedMode = ShellSettings.Settings.actionDrawerTopLeftMode == ShellSettings.Settings.Pinned;
} else {
root.openToPinnedMode = ShellSettings.Settings.actionDrawerTopRightMode == ShellSettings.Settings.Pinned;
}
if (root.intendedToBeVisible) {
// ensure the action drawer state is consistent
root.closeImmediately();
}
actionDrawer.offset = 0;
actionDrawer.oldOffset = 0;
intendedToBeVisible = true;
}
}
function endSwipe() {
root.dragging = false;
root.updateState();
}
function moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY) {
root.offset += deltaY;
}
onSwipeStarted: (point) => startSwipeWithPoint(point)
onSwipeEnded: endSwipe()
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
onTouchpadScrollStarted: startSwipe()
onTouchpadScrollEnded: endSwipe()
onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveSwipe(totalDeltaX, totalDeltaY, deltaX, deltaY)
ContentContainer {
id: contentContainer
anchors.fill: parent
2024-07-27 03:47:44 +00:00
opacity: root.opened || swipeArea.moving || drawerAnimation.running || root.offset > 0 || root.intendedToBeVisible ? 1 : 0
actionDrawer: root
quickSettingsModel: root.quickSettingsModel
}
}
}