/* * SPDX-FileCopyrightText: 2014 Marco Martin * SPDX-FileCopyrightText: 2021 Devin Lin * * 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.components 3.0 as PlasmaComponents import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.kirigami 2.20 as Kirigami import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS import org.kde.plasma.private.mobileshell as MobileShell import 'actiondrawer/private' Item { id: root /* * The intended visibility of the action drawer. * * This is separate from "visible" in order to avoid having to set * item visibility when it's 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 /** * The model type for the notification widget. */ property var notificationModelType: MobileShell.NotificationsModelType.NotificationsModel /** * 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). * * The permissionsRequested() signal emits when authentication is requested. */ property bool restrictedPermissions: false /** * The amount of pixels moved by touch/mouse in the process of opening/closing the panel. */ property real offset: 0 /** * Same as offset value except this adds resistance when passing the open position of the current drawer state. */ readonly property real offsetResistance: { if (!openToPinnedMode) { return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset); } else if (!opened) { return root.calculateResistance(offset, contentContainer.minimizedQuickSettingsOffset); } else { return root.calculateResistance(offset, contentContainer.maximizedQuickSettingsOffset); } } /** * Whether the panel is being dragged. */ property bool dragging: false /** * Whether the panel is open after touch/mouse release from the first opening swipe. */ property bool opened: false /** * Whether the panel should open to pinned mode first, with a second stroke needed to full open. * Only applies to portrait mode. */ property bool openToPinnedMode: true /** * Direction the panel is currently moving in. */ property int direction: MobileShell.Direction.None /** * The mode of the action drawer (portrait or landscape). */ property int mode: (height > width && width <= largePortraitThreshold) ? MobileShell.ActionDrawer.Portrait : MobileShell.ActionDrawer.Landscape /** * 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 enum Mode { Portrait = 0, Landscape } /** * Emitted when the drawer has closed. */ signal drawerClosed() /** * Emitted when the drawer has opened. */ signal drawerOpened() /** * Emitted when permissions are requested (ex. unlocking the phone). * * Only gets emitted when restrictedPermissions is set to true. */ signal permissionsRequested() /** * Runs the held notification action that was pending for authentication. * * Should be called by users if authentication is successful after permissionsRequested() was emitted. */ signal runPendingNotificationAction() property real oldOffset onOffsetChanged: { if (offset < 0) { offset = 0; } root.direction = (oldOffset === offset) ? MobileShell.Direction.None : (offset > oldOffset ? MobileShell.Direction.Down : MobileShell.Direction.Up); oldOffset = offset; // close panel immediately after panel is not shown, and the flickable is not being dragged if (opened && root.offset <= 0 && !contentContainer.swipeAreaMoving && !drawerAnimation.running) { root.state = ""; offset = 0; focus = false; root.opened = false; root.updateState(); } } // calculates offset resistance for the action drawer overshoots it's open position function calculateResistance(value : double, threshold : int) : double { if (value > threshold) { return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((root.height - threshold) * 15), 0.35)); } else { return value; } } function cancelAnimations() { root.state = ""; } function open() { cancelAnimations(); if (openToPinnedMode) { root.state = "open"; // go to pinned height } else { root.state = "expand"; // go to maximized height } } function closeImmediately() { cancelAnimations(); offset = 0; root.state = "close"; } function close() { cancelAnimations(); root.state = "close"; } function expand() { cancelAnimations(); root.state = "expand"; } function updateState() { let openThreshold = Kirigami.Units.gridUnit; if (root.offset <= 0) { // 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) { // if the panel has not been opened yet, run open animation only if drag passed threshold (root.offset < openThreshold) ? close() : open(); } 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; } } } } } } // action drawer ui content ContentContainer { id: contentContainer anchors.fill: parent actionDrawer: root quickSettingsModel: root.quickSettingsModel } }