Commit 57192b77 authored by Devin Lin's avatar Devin Lin 🎨
Browse files

actiondrawer: Add restricted permission mode, and implement on lockscreen

parent 3969973e
Pipeline #160903 passed with stages
in 1 minute and 22 seconds
......@@ -68,6 +68,7 @@ void MobileShellPlugin::registerTypes(const char *uri)
// /actiondrawer
qmlRegisterType(resolvePath("actiondrawer/ActionDrawer.qml"), uri, 1, 0, "ActionDrawer");
qmlRegisterType(resolvePath("actiondrawer/ActionDrawerOpenSurface.qml"), uri, 1, 0, "ActionDrawerOpenSurface");
qmlRegisterType(resolvePath("actiondrawer/ActionDrawerWindow.qml"), uri, 1, 0, "ActionDrawerWindow");
// /components
qmlRegisterType(resolvePath("components/BaseItem.qml"), uri, 1, 0, "BaseItem");
......
......@@ -13,29 +13,35 @@ import QtQuick.Window 2.2
import org.kde.plasma.core 2.0 as PlasmaCore
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 1.0 as MobileShell
import "../components" as Components
/**
* Swipe top left - minimized quick settings, fully shown notifications list
* Swipe top right - full quick settings, minimized notifications list
* Swiping up and down on notifications list toggle minimized/maximized
* Swiping up and down on panel hides and shows the panel
*/
NanoShell.FullScreenOverlay {
id: window
Item {
id: root
/**
* The model for the notification widget.
*/
property var notificationModel
/**
* The model type for the notification widget.
*/
property var notificationModelType: MobileShell.NotificationsModelType.NotificationsModel
/**
* 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.
*/
......@@ -56,6 +62,9 @@ NanoShell.FullScreenOverlay {
*/
property int direction: Components.Direction.None
/**
* The mode of the action drawer (portrait or landscape).
*/
property int mode: (height > width && width <= largePortraitThreshold) ? ActionDrawer.Portrait : ActionDrawer.Landscape
/**
......@@ -68,34 +77,48 @@ NanoShell.FullScreenOverlay {
Landscape
}
width: Screen.width
height: Screen.height
/**
* Emitted when the drawer has closed.
*/
signal drawerClosed()
color: "transparent"
/**
* 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()
onOpenedChanged: {
if (opened) flickable.focus = true;
}
onActiveChanged: {
if (!active) {
close();
}
}
property real oldOffset
onOffsetChanged: {
if (offset < 0) {
offset = 0;
}
window.direction = (oldOffset === offset)
root.direction = (oldOffset === offset)
? Components.Direction.None
: (offset > oldOffset ? Components.Direction.Down : Components.Direction.Up);
oldOffset = offset;
// close panel immediately after panel is not shown, and the flickable is not being dragged
if (opened && window.offset <= 0 && !flickable.dragging && !closeAnim.running && !openAnim.running) {
window.updateState();
if (opened && root.offset <= 0 && !flickable.dragging && !closeAnim.running && !openAnim.running) {
root.updateState();
focus = false;
}
}
......@@ -125,25 +148,25 @@ NanoShell.FullScreenOverlay {
cancelAnimations();
let openThreshold = PlasmaCore.Units.gridUnit;
if (window.offset <= 0) {
if (root.offset <= 0) {
// close immediately, so that we don't have to wait PlasmaCore.Units.longDuration
window.visible = false;
root.visible = false;
close();
} else if (window.direction === Components.Direction.None || !window.opened) {
if (window.offset < openThreshold) {
} else if (root.direction === Components.Direction.None || !root.opened) {
if (root.offset < openThreshold) {
close();
} else {
open();
}
} else if (window.offset > contentContainerLoader.maximizedQuickSettingsOffset) {
} else if (root.offset > contentContainerLoader.maximizedQuickSettingsOffset) {
expand();
} else if (window.offset > contentContainerLoader.minimizedQuickSettingsOffset) {
if (window.direction === Components.Direction.Down) {
} else if (root.offset > contentContainerLoader.minimizedQuickSettingsOffset) {
if (root.direction === Components.Direction.Down) {
expand();
} else {
open();
}
} else if (window.direction === Components.Direction.Down) {
} else if (root.direction === Components.Direction.Down) {
open();
} else {
close();
......@@ -161,8 +184,8 @@ NanoShell.FullScreenOverlay {
easing.type: Easing.InOutQuad
to: 0
onFinished: {
window.visible = false;
window.opened = false;
root.visible = false;
root.opened = false;
}
}
PropertyAnimation on offset {
......@@ -170,31 +193,31 @@ NanoShell.FullScreenOverlay {
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
to: contentContainerLoader.minimizedQuickSettingsOffset
onFinished: window.opened = true
onFinished: root.opened = true
}
PropertyAnimation on offset {
id: expandAnim
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
to: contentContainerLoader.maximizedQuickSettingsOffset
onFinished: window.opened = true;
onFinished: root.opened = true;
}
Flickable {
id: flickable
anchors.fill: parent
contentWidth: window.width
contentHeight: window.height + 999999
contentWidth: root.width
contentHeight: root.height + 999999
contentY: contentHeight / 2
// if the recent window.offset change was due to this flickable
// if the recent root.offset change was due to this flickable
property bool offsetChangedDueToContentY: false
Connections {
target: window
target: root
function onOffsetChanged() {
if (!flickable.offsetChangedDueToContentY) {
// ensure the flickable's contentY is not moving when other sources change window.offset
// ensure the flickable's contentY is not moving when other sources change root.offset
flickable.cancelFlick();
}
flickable.offsetChangedDueToContentY = false;
......@@ -204,34 +227,34 @@ NanoShell.FullScreenOverlay {
property real oldContentY
onContentYChanged: {
offsetChangedDueToContentY = true;
window.offset += oldContentY - contentY;
root.offset += oldContentY - contentY;
oldContentY = contentY;
}
onMovementStarted: {
window.cancelAnimations();
window.dragging = true;
root.cancelAnimations();
root.dragging = true;
}
onFlickStarted: window.dragging = true;
onFlickStarted: root.dragging = true;
onMovementEnded: {
window.dragging = false;
window.updateState();
root.dragging = false;
root.updateState();
}
onFlickEnded: {
window.dragging = true;
window.updateState();
root.dragging = true;
root.updateState();
}
onDraggingChanged: {
if (!dragging) {
window.dragging = false;
root.dragging = false;
flickable.cancelFlick();
window.updateState();
root.updateState();
}
}
// the flickable is only used to measure drag changes, we implement our own UI component movements
// the window element is not affected by contentY changes (it's effectively anchored to the flickable)
// the root element is not affected by contentY changes (it's effectively anchored to the flickable)
Loader {
id: contentContainerLoader
......@@ -239,27 +262,27 @@ NanoShell.FullScreenOverlay {
property real maximizedQuickSettingsOffset: item ? item.maximizedQuickSettingsOffset : 0
y: flickable.contentY
width: window.width
height: window.height
width: root.width
height: root.height
sourceComponent: window.mode == ActionDrawer.Portrait ? portraitContentContainer : landscapeContentContainer
sourceComponent: root.mode == ActionDrawer.Portrait ? portraitContentContainer : landscapeContentContainer
}
Component {
id: portraitContentContainer
PortraitContentContainer {
actionDrawer: window
width: window.width
height: window.height
actionDrawer: root
width: root.width
height: root.height
}
}
Component {
id: landscapeContentContainer
LandscapeContentContainer {
actionDrawer: window
width: window.width
height: window.height
actionDrawer: root
width: root.width
height: root.height
}
}
}
......
/*
* SPDX-FileCopyrightText: 2022 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.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
import "../components" as Components
/**
* Window with the ActionDrawer component embedded in it.
*
* Used for overlaying the ActionDrawer if the original window does not cover
* the whole screen.
*/
NanoShell.FullScreenOverlay {
id: window
/**
* The ActionDrawer component.
*/
property alias actionDrawer: drawer
visible: drawer.visible
width: Screen.width
height: Screen.height
color: "transparent"
onActiveChanged: {
if (!active) {
drawer.close();
}
}
ActionDrawer {
id: drawer
anchors.fill: parent
}
}
......@@ -95,7 +95,18 @@ PlasmaCore.ColorScope {
MobileShell.NotificationsWidget {
id: notificationWidget
historyModel: root.actionDrawer.notificationModel
historyModelType: root.actionDrawer.notificationModelType
notificationSettings: root.actionDrawer.notificationSettings
actionsRequireUnlock: root.actionDrawer.restrictedPermissions
onUnlockRequested: root.actionDrawer.permissionsRequested()
Connections {
target: root.actionDrawer
function onRunPendingNotificationAction() {
notificationWidget.runPendingAction();
}
}
// don't allow notifications widget to get too wide
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
......
......@@ -79,7 +79,18 @@ PlasmaCore.ColorScope {
MobileShell.NotificationsWidget {
id: notificationWidget
historyModel: root.actionDrawer.notificationModel
historyModelType: root.actionDrawer.notificationModelType
notificationSettings: root.actionDrawer.notificationSettings
actionsRequireUnlock: root.actionDrawer.restrictedPermissions
onUnlockRequested: root.actionDrawer.permissionsRequested()
Connections {
target: root.actionDrawer
function onRunPendingNotificationAction() {
notificationWidget.runPendingAction();
}
}
anchors {
top: quickSettings.top
......
......@@ -69,6 +69,8 @@ Item {
padding: PlasmaCore.Units.smallSpacing
contentItem: QuickSettingsFullDelegate {
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
......@@ -119,6 +121,8 @@ Item {
visible: index <= root.minimizedColumns
contentItem: QuickSettingsMinimizedDelegate {
restrictedPermissions: actionDrawer.restrictedPermissions
text: modelData.text
status: modelData.status
icon: modelData.icon
......
......@@ -20,6 +20,8 @@ import "../../components" as Components
Components.BaseItem {
id: root
required property bool restrictedPermissions
// Model interface
required property string text
required property string status
......@@ -61,7 +63,7 @@ Components.BaseItem {
root.toggle();
} else if (root.toggleFunction) {
root.toggleFunction();
} else if (root.settingsCommand) {
} else if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested();
MobileShell.HomeScreenControls.openAppLaunchAnimation(
root.icon,
......@@ -74,7 +76,7 @@ Components.BaseItem {
}
function delegatePressAndHold() {
if (root.settingsCommand) {
if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested();
MobileShell.HomeScreenControls.openAppLaunchAnimation(
root.icon,
......
......@@ -85,6 +85,9 @@ Components.BaseItem {
backgroundColor: "transparent"
showSecondRow: true
showDropShadow: false
// security reasons, system tray also doesn't work on lockscreen
disableSystemTray: actionDrawer.restrictedPermissions
}
QuickSettings {
......
......@@ -77,6 +77,9 @@ Components.BaseItem {
showSecondRow: false
showDropShadow: false
showTime: false
// security reasons, system tray also doesn't work on lockscreen
disableSystemTray: actionDrawer.restrictedPermissions
}
PlasmaComponents.ScrollView {
......
......@@ -60,8 +60,17 @@ Item {
WatchedNotificationsModel // used on the lockscreen
}
/**
* Signal emitted when authentication is requested for an action.
* Listeners should call runPendingAction() if authentication is successful.
*
* Only emitted if actionsRequireUnlock is enabled.
*/
signal unlockRequested()
/**
* Run pending action that was pending for authentication when unlockRequested() was emitted.
*/
function runPendingAction() {
list.pendingNotificationWithAction.runPendingAction();
}
......
......@@ -16,6 +16,7 @@
<file>qml/actiondrawer/ActionDrawer.qml</file>
<file>qml/actiondrawer/ActionDrawerOpenSurface.qml</file>
<file>qml/actiondrawer/ActionDrawerWindow.qml</file>
<file>qml/actiondrawer/LandscapeContentContainer.qml</file>
<file>qml/actiondrawer/PortraitContentContainer.qml</file>
......
......@@ -41,7 +41,7 @@ Item {
Binding {
target: MobileShell.TopPanelControls
property: "inSwipe"
value: drawer.dragging
value: drawer.actionDrawer.dragging
}
Binding {
target: MobileShell.TopPanelControls
......@@ -62,10 +62,10 @@ Item {
swipeArea.updateOffset(offsetY);
}
function onCloseActionDrawer() {
drawer.close();
drawer.actionDrawer.close();
}
function onOpenActionDrawer() {
drawer.open();
drawer.actionDrawer.open();
}
}
......@@ -88,30 +88,30 @@ Item {
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: drawer
actionDrawer: drawer.actionDrawer
anchors.fill: parent
}
// swipe-down drawer component
MobileShell.ActionDrawer {
MobileShell.ActionDrawerWindow {
id: drawer
notificationSettings: NotificationManager.Settings {}
actionDrawer.notificationSettings: NotificationManager.Settings {}
notificationModel: NotificationManager.Notifications {
actionDrawer.notificationModel: NotificationManager.Notifications {
showExpired: true
showDismissed: true
showJobs: drawer.notificationSettings.jobsInNotifications
showJobs: drawer.actionDrawer.notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
groupMode: NotificationManager.Notifications.GroupApplicationsFlat
groupLimit: 2
expandUnread: true
blacklistedDesktopEntries: drawer.notificationSettings.historyBlacklistedApplications
blacklistedNotifyRcNames: drawer.notificationSettings.historyBlacklistedServices
blacklistedDesktopEntries: drawer.actionDrawer.notificationSettings.historyBlacklistedApplications
blacklistedNotifyRcNames: drawer.actionDrawer.notificationSettings.historyBlacklistedServices
urgencies: {
var urgencies = NotificationManager.Notifications.CriticalUrgency
| NotificationManager.Notifications.NormalUrgency;
if (drawer.notificationSettings.lowPriorityHistory) {
if (drawer.actionDrawer.notificationSettings.lowPriorityHistory) {
urgencies |= NotificationManager.Notifications.LowUrgency;
}
return urgencies;
......
/*
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.notificationmanager 1.0 as NotificationManager
Loader {
id: root
required property real openFactor
readonly property real statusBarHeight: PlasmaCore.Units.gridUnit * 1.25
property var notificationsModel: []
signal passwordRequested()
active: true
asynchronous: true
sourceComponent: Item {
MobileShell.StatusBar {
id: statusBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: root.statusBarHeight
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: false
disableSystemTray: true // HACK: prevent SIGABRT
}
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: drawer
anchors.fill: statusBar
}
// swipe-down drawer component
MobileShell.ActionDrawer {
id: drawer
anchors.fill: parent