From d8399e88bed2ff778a722882e568d6b9336e0e3b Mon Sep 17 00:00:00 2001 From: Yari Polla Date: Fri, 24 Jun 2022 17:44:30 +0200 Subject: [PATCH] toppanel/notifications: add clean all and do not disturb Hi, this MR adds "clear all" and "do not disturb" buttons to `NotificationsWidget`. Few other corrections have been made: - Transitions should now work correctly, plus they can be disabled via `MobileShell.MobileShellSettings.animationsEnabled`; - Loaders are now asynchronous; - List items have now their own margins, in order to not clip their shadows.\ It only remains to implement a context menu to set do not disturb mode for a given period of time. I think it's a secondary feature on which we can work later, but in case tell me what to do. Unfortunately I can't test the lockscreen on a phone at the moment, and I cannot take a performance test as well. It would be great if someone tested all these things, otherwise I'll provide as soon as possible. Closes: https://invent.kde.org/plasma/plasma-mobile/-/issues/134 --- .../LandscapeContentContainer.qml | 1 + .../actiondrawer/PortraitContentContainer.qml | 5 +- .../notifications/NotificationsWidget.qml | 229 ++++++++++++++---- .../lockscreen/NotificationsComponent.qml | 4 + 4 files changed, 194 insertions(+), 45 deletions(-) diff --git a/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml b/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml index 7a55bb7f..9880ae1a 100644 --- a/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml +++ b/components/mobileshell/qml/actiondrawer/LandscapeContentContainer.qml @@ -71,6 +71,7 @@ PlasmaCore.ColorScope { top: mediaWidget.bottom topMargin: 0 bottom: parent.bottom + bottomMargin: 0 right: quickSettings.left left: parent.left } diff --git a/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml b/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml index 6ddd5a8e..a585f293 100644 --- a/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml +++ b/components/mobileshell/qml/actiondrawer/PortraitContentContainer.qml @@ -43,7 +43,7 @@ PlasmaCore.ColorScope { color: Qt.rgba(PlasmaCore.Theme.backgroundColor.r, PlasmaCore.Theme.backgroundColor.g, PlasmaCore.Theme.backgroundColor.b, - notificationWidget.hasNotifications ? 0.95 : 0.7) + 0.95) Behavior on color { ColorAnimation { duration: PlasmaCore.Units.longDuration } } opacity: Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset)) } @@ -114,11 +114,8 @@ PlasmaCore.ColorScope { top: quickSettings.top topMargin: quickSettings.height + translate.y bottom: parent.bottom - bottomMargin: PlasmaCore.Units.largeSpacing left: parent.left - leftMargin: PlasmaCore.Units.largeSpacing right: parent.right - rightMargin: PlasmaCore.Units.largeSpacing } opacity: applyMinMax(root.actionDrawer.offset / root.minimizedQuickSettingsOffset) } diff --git a/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml index 63bec1fa..dc0f98fd 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml @@ -6,7 +6,7 @@ */ import QtQuick 2.15 -import QtQuick.Layouts 1.1 +import QtQuick.Layouts 1.15 import QtQuick.Window 2.2 import QtGraphicalEffects 1.12 @@ -29,17 +29,17 @@ Item { /** * The notification model for the widget. */ - property var historyModel: [] + property var historyModel /** * The type of notification model used for the widget. */ - property int historyModelType: NotificationsModelType.NotificationsModel + property int historyModelType /** * The notification model settings for the widget. */ - property var notificationSettings: NotificationManager.Settings {} + property var notificationSettings /** * Whether invoking notification actions requires authentiation of some sort. @@ -55,6 +55,8 @@ Item { */ readonly property bool hasNotifications: list.count > 0 + readonly property bool doNotDisturbModeEnabled: !isNaN(notificationSettings.notificationsInhibitedUntil) + enum ModelType { NotificationsModel, // used in the logged-in shell WatchedNotificationsModel // used on the lockscreen @@ -85,8 +87,29 @@ Item { */ function clearHistory() { historyModel.clear(NotificationManager.Notifications.ClearExpired); + + if (historyModel.count === 0) { + backgroundClicked(); + } } + /** + * Toggles Do Not Disturb mode. + */ + function toggleDoNotDisturbMode() { + if (doNotDisturbModeEnabled) { + notificationSettings.defaults(); + } else { + var until = new Date(); + + until.setFullYear(until.getFullYear() + 1); + + notificationSettings.notificationsInhibitedUntil = until; + } + + notificationSettings.save(); + } + /** * Open the system notification settings. */ @@ -102,33 +125,49 @@ Item { intervalAlignment: PlasmaCore.Types.AlignToMinute } + // implement background clicking signal + MouseArea { + anchors.fill: parent + onClicked: backgroundClicked() + z: -1 // ensure that this is below notification items so we don't steal button clicks + } + ListView { id: list model: historyModel - currentIndex: -1 + + clip: true + + currentIndex: 0 property var pendingNotificationWithAction + + readonly property int animationDuration: MobileShell.MobileShellSettings.animationsEnabled ? PlasmaCore.Units.longDuration : 0 + // If a screen overflow occurs, fix height in order to maintain tool buttons in place. + readonly property bool listOverflowing: contentItem.childrenRect.height + toolButtons.height + bottomMargin + spacing >= root.height + + bottomMargin: spacing * 2 + + height: count === 0 ? 0 : listOverflowing ? root.height - toolButtons.height - bottomMargin : contentItem.childrenRect.height + spacing + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + boundsBehavior: Flickable.StopAtBounds spacing: Kirigami.Units.largeSpacing - - anchors.fill: parent // TODO keyboard focus highlightMoveDuration: 0 highlightResizeDuration: 0 highlight: Item {} - - section { - property: "isInGroup" - criteria: ViewSection.FullString - } - // implement background clicking signal - MouseArea { - anchors.fill: parent - onClicked: root.backgroundClicked() - z: -1 // ensure that this is below notification items so we don't steal button clicks + section { + property: "isGroup" + criteria: ViewSection.FullString } PlasmaExtras.PlaceholderMessage { @@ -149,26 +188,15 @@ Item { } } + // Run every time an item is visually added to the list, thus when `Show n more` button is clicked as well. add: Transition { - SequentialAnimation { - PropertyAction { property: "opacity"; value: 0 } - PauseAnimation { duration: PlasmaCore.Units.longDuration } - ParallelAnimation { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: PlasmaCore.Units.longDuration } - NumberAnimation { property: "height"; from: 0; duration: PlasmaCore.Units.longDuration } - } - } + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: list.animationDuration } } - addDisplaced: Transition { - NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + // Run every time an item is displaced, such as when the order is scrambled due to a group expansion. + displaced: Transition { + NumberAnimation { properties: "y"; duration: list.animationDuration } } - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: PlasmaCore.Units.longDuration } - NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } - } - } - + function isRowExpanded(row) { var idx = historyModel.index(row, 0); return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); @@ -191,19 +219,38 @@ Item { } } } + + // Instantly re-align items after group expansion. + forceLayout(); } delegate: Loader { id: delegateLoader - width: list.width + + anchors { + left: parent ? parent.left : undefined + leftMargin: PlasmaCore.Units.largeSpacing + right: parent ? parent.right : undefined + rightMargin: PlasmaCore.Units.largeSpacing + } + + height: model.isGroup ? groupDelegate.height : notificationDelegate.height sourceComponent: model.isGroup ? groupDelegate : notificationDelegate + asynchronous: true required property var model required property int index + // We have to do this here in order to control the animation before the item is completely removed + ListView.onRemove: SequentialAnimation { + PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: true } + NumberAnimation { target: delegateLoader; property: "opacity"; to: 0.0; duration: list.animationDuration } + PropertyAction { target: delegateLoader; property: "ListView.delayRemove"; value: false } + } + Component { id: groupDelegate - NotificationGroupHeader { + NotificationGroupHeader { applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" @@ -213,12 +260,16 @@ Item { Component { id: notificationDelegate - ColumnLayout { + + Column { spacing: PlasmaCore.Units.smallSpacing + height: notificationItem.height + showMoreLoader.height + NotificationItem { id: notificationItem - Layout.fillWidth: true + width: parent.width + height: implicitHeight model: delegateLoader.model modelIndex: delegateLoader.index @@ -233,16 +284,37 @@ Item { } } + // Every item has got an instance of this loader, but it becomes active only for last ones that take place in big enough groups. Loader { + id: showMoreLoader + height: visible ? implicitHeight : 0 + opacity: 0.0 visible: active + + asynchronous: true + active: { // if we have the WatchedNotificationsModel, we don't have notification grouping support if (typeof model.groupChildrenCount === 'undefined') return false; - + return (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) - && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section + && delegateLoader.ListView.nextSection != delegateLoader.ListView.section; + } + + // state + transition: animates the item when it becomes visible. Fade off is handled by above ListView.onRemove. + states: State { + name: "VISIBLE" + when: showMoreLoader.status == Loader.Ready + PropertyChanges { target: showMoreLoader; opacity: 1.0 } + } + transitions: Transition { + to: "VISIBLE" + SequentialAnimation { + PauseAnimation { duration: list.animationDuration * 2 } + NumberAnimation { properties: "opacity"; duration: list.animationDuration } + } } sourceComponent: PlasmaComponents3.ToolButton { @@ -250,11 +322,86 @@ Item { text: model.isGroupExpanded ? i18n("Show Fewer") : i18nc("Expand to show n more notifications", "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) - onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) + onClicked: { + list.setGroupExpanded(model.index, !model.isGroupExpanded) + } } } } } } } + + Item { + id: toolButtons + + visible: !root.actionsRequireUnlock + + width: root.width + height: toolLayout.implicitHeight + spacer.height + + anchors { + top: list.bottom + left: parent.left + right: parent.right + } + + Rectangle { + id: spacer + + visible: list.listOverflowing + + anchors { + top: toolButtons.top + left: toolButtons.left + right: toolButtons.right + } + + height: 1 + + opacity: 0.25 + color: PlasmaCore.Theme.textColor + } + + RowLayout { + id: toolLayout + + anchors { + top: spacer.bottom + topMargin: list.spacing + left: parent.left + leftMargin: PlasmaCore.Units.smallSpacing + right: parent.right + rightMargin: PlasmaCore.Units.smallSpacing + bottom: parent.bottom + bottomMargin: list.spacing + } + + PlasmaComponents3.ToolButton { + id: doNotDisturbButton + + Layout.alignment: hasNotifications ? Qt.AlignLeft : Qt.AlignHCenter + + font.bold: true + + icon.name: doNotDisturbModeEnabled ? "notifications" : "notifications-disabled" + text: doNotDisturbModeEnabled ? "Enable Notifications" : "Do Not Disturb" + onClicked: toggleDoNotDisturbMode() + } + + PlasmaComponents3.ToolButton { + id: clearButton + + Layout.alignment: Qt.AlignRight + + visible: hasNotifications + + font.bold: true + + icon.name: "edit-clear-history" + text: "Clear All Notifications" + onClicked: clearHistory() + } + } + } } diff --git a/look-and-feel/contents/lockscreen/NotificationsComponent.qml b/look-and-feel/contents/lockscreen/NotificationsComponent.qml index 93c8d20e..e0b8b7a4 100644 --- a/look-and-feel/contents/lockscreen/NotificationsComponent.qml +++ b/look-and-feel/contents/lockscreen/NotificationsComponent.qml @@ -13,10 +13,13 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.notificationmanager 1.1 as Notifications import org.kde.plasma.private.mobileshell 1.0 as MobileShell +import org.kde.notificationmanager 1.0 as NotificationManager + Rectangle { id: root required property var lockScreenState property var notificationsModel: [] + property var notificationSettings: NotificationManager.Settings {} readonly property bool notificationsShown: notificationsList.hasNotifications @@ -61,6 +64,7 @@ Rectangle { historyModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel actionsRequireUnlock: true historyModel: root.notificationsModel + notificationSettings: root.notificationSettings property bool requestNotificationAction: false