actiondrawer: Add restricted permission mode, and implement on lockscreen

This commit is contained in:
Devin Lin 2022-04-07 14:11:08 -04:00
parent 3969973e51
commit 57192b772b
18 changed files with 323 additions and 116 deletions

View file

@ -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");

View file

@ -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
}
}
}

View file

@ -0,0 +1,48 @@
/*
* 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
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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();
}

View file

@ -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>

View 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;

View file

@ -0,0 +1,87 @@
/*
* 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
restrictedPermissions: true
notificationSettings: NotificationManager.Settings {}
notificationModel: root.notificationsModel
notificationModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
property bool requestNotificationAction: false
onPermissionsRequested: {
requestNotificationAction = true;
root.passwordRequested();
}
}
Connections {
target: authenticator
function onSucceeded() {
// run pending action if successfully unlocked
if (drawer.requestNotificationAction) {
drawer.runPendingAction();
drawer.requestNotificationAction = false;
}
}
function onFailed() {
drawer.requestNotificationAction = false;
}
}
}
}

View file

@ -42,6 +42,17 @@ PlasmaCore.ColorScope {
blur: root.notificationsShown || root.drawerOpen // only blur once animation finished for performance
}
}
// header bar and action drawer
HeaderComponent {
id: headerBar
z: 1 // on top of flick area
anchors.fill: parent
openFactor: flickable.openFactor
notificationsModel: notifModel
onPasswordRequested: root.askPassword()
}
FlickContainer {
id: flickable
@ -69,23 +80,20 @@ PlasmaCore.ColorScope {
height: flickable.height
y: flickable.contentY // effectively anchored to the screen
// header bar
StatusBarComponent {
id: statusBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
opacity: 1 - flickable.openFactor
}
LockScreenNarrowContent {
id: phoneComponent
visible: !isWidescreen
active: visible
opacity: 1 - flickable.openFactor
fullHeight: root.height
notificationsModel: notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: root.askPassword()
anchors.top: parent.top
anchors.bottom: scrollUpIcon.top
anchors.left: parent.left
@ -101,7 +109,13 @@ PlasmaCore.ColorScope {
active: visible
opacity: 1 - flickable.openFactor
anchors.top: statusBar.bottom
notificationsModel: notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: root.askPassword()
anchors.topMargin: headerBar.statusBarHeight
anchors.top: parent.top
anchors.bottom: scrollUpIcon.top
anchors.left: parent.left
anchors.right: parent.right

View file

@ -14,11 +14,15 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Loader {
id: root
asynchronous: true
property var notificationsModel: []
property real fullHeight
property bool notificationsShown: false
property real fullHeight
signal passwordRequested()
// avoid topMargin animation when item is being loaded
onLoaded: loadTimer.restart();
Timer {
@ -29,6 +33,7 @@ Loader {
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
asynchronous: true
sourceComponent: Item {
ColumnLayout {
id: column
@ -62,6 +67,8 @@ Loader {
NotificationsComponent {
id: notificationComponent
notificationsModel: root.notificationsModel
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins
@ -69,6 +76,7 @@ Loader {
leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown
}
}

View file

@ -13,14 +13,18 @@ import org.kde.notificationmanager 1.1 as Notifications
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Loader {
id: tabletComponent
asynchronous: true
id: root
property var notificationsModel: []
property bool notificationsShown: false
signal passwordRequested()
asynchronous: true
sourceComponent: Item {
Item {
id: tabletClockComponent
id: clock
width: parent.width / 2
anchors {
top: parent.top
@ -54,13 +58,14 @@ Loader {
anchors {
top: parent.top
bottom: parent.bottom
left: tabletClockComponent.right
left: clock.right
right: parent.right
rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsComponent {
id: notificationComponent
notificationsModel: root.notificationsModel
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true
@ -75,6 +80,7 @@ Loader {
bottomMargin: PlasmaCore.Units.gridUnit
topMargin: PlasmaCore.Units.gridUnit
onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown
}
}

View file

@ -16,8 +16,12 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Rectangle {
id: rect
property var notificationsModel: []
readonly property bool notificationsShown: notificationsList.hasNotifications
signal passwordRequested()
property real leftMargin: 0
property real rightMargin: 0
property real topMargin: 0
@ -54,13 +58,13 @@ Rectangle {
historyModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
actionsRequireUnlock: true
historyModel: notifModel
historyModel: rect.notificationsModel
property bool requestNotificationAction: false
onUnlockRequested: {
requestNotificationAction = true;
root.askPassword();
root.passwordRequested();
}
}
}

View file

@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Loader {
active: true
asynchronous: true
height: PlasmaCore.Units.gridUnit * 1.25
sourceComponent: MobileShell.StatusBar {
id: statusBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: false
disableSystemTray: true // HACK: prevent SIGABRT
}
}