/* * SPDX-FileCopyrightText: 2021 Devin Lin * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.2 import Qt5Compat.GraphicalEffects import org.kde.kirigami 2.12 as Kirigami import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.plasma5support 2.0 as P5Support import org.kde.plasma.private.mobileshell 1.0 as MobileShell import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.notificationmanager 1.0 as NotificationManager /** * Embeddable notification list widget optimized for mobile and touch. * Used on the lockscreen and action drawer. */ Item { id: root /** * The notification model for the widget. */ property var historyModel /** * The type of notification model used for the widget. */ property int historyModelType /** * The notification model settings for the widget. */ property var notificationSettings /** * Whether invoking notification actions requires authentiation of some sort. * * If set to true, any attempted invoking will trigger the unlockRequested() signal. * Any consumers can then call the runPendingAction() function if authenticated to proceed * executing the notification action. */ property bool actionsRequireUnlock: false /** * Whether the widget has notifications. */ 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 } /** * 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() /** * Emitted when the background is clicked (not a notification or other element). */ signal backgroundClicked() /** * Run pending action that was pending for authentication when unlockRequested() was emitted. */ function runPendingAction() { list.pendingNotificationWithAction.runPendingAction(); } /** * Clears the history of the notification model. */ 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. */ function openNotificationSettings() { MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications"); } P5Support.DataSource { id: timeDataSource engine: "time" connectedSources: ["Local"] interval: 60000 // 1 min 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 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 + spacing >= root.height bottomMargin: spacing height: count === 0 ? 0 : (listOverflowing ? root.height - toolButtons.height : contentItem.childrenRect.height + bottomMargin) anchors { top: parent.top left: parent.left right: parent.right } boundsBehavior: Flickable.StopAtBounds spacing: Kirigami.Units.largeSpacing // TODO keyboard focus highlightMoveDuration: 0 highlightResizeDuration: 0 highlight: Item {} section { property: "isGroup" criteria: ViewSection.FullString } PlasmaExtras.PlaceholderMessage { anchors.centerIn: parent width: parent.width - (PlasmaCore.Units.largeSpacing * 4) text: i18n("Notification service not available") visible: list.count === 0 && !NotificationManager.Server.valid && historyModelType === NotificationsModelType.NotificationsModel PlasmaComponents3.Label { // Checking valid to avoid creating ServerInfo object if everything is alright readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner : null // PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, so we can use Layout.whatever properties here Layout.fillWidth: true wrapMode: Text.WordWrap text: currentOwner ? i18nc("Vendor and product name", "Notifications are currently provided by '%1 %2'", currentOwner.vendor, currentOwner.name) : "" visible: currentOwner && currentOwner.vendor && currentOwner.name } } // Run every time an item is visually added to the list, thus when `Show n more` button is clicked as well. add: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: list.animationDuration } } // 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 } } function isRowExpanded(row) { var idx = historyModel.index(row, 0); return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); } function setGroupExpanded(row, expanded) { var rowIdx = historyModel.index(row, 0); var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); // If the current item went away when the group collapsed, scroll to the group heading if (!persistentRowIdx || !persistentRowIdx.valid) { if (persistentGroupIdx && persistentGroupIdx.valid) { list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); // When closed via keyboard, also set a sane current index if (list.currentIndex > -1) { list.currentIndex = persistentGroupIdx.row; } } } // Instantly re-align items after group expansion. forceLayout(); } delegate: Loader { id: delegateLoader 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 { applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" timeSource: timeDataSource } } Component { id: notificationDelegate Column { spacing: PlasmaCore.Units.smallSpacing height: notificationItem.height + showMoreLoader.height NotificationItem { id: notificationItem width: parent.width height: implicitHeight model: delegateLoader.model modelIndex: delegateLoader.index notificationsModel: root.historyModel notificationsModelType: root.historyModelType timeSource: timeDataSource requestToInvoke: root.actionsRequireUnlock onRunActionRequested: { list.pendingNotificationWithAction = notificationItem; root.unlockRequested(); } } // 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; } // 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 { icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down" 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) } } } } } } } Item { id: toolButtons height: visible ? spacer.height + toolLayout.height + toolLayout.anchors.topMargin + toolLayout.anchors.bottomMargin : 0 // do not show on lockscreen visible: !root.actionsRequireUnlock anchors { top: list.bottom left: parent.left right: parent.right } Rectangle { id: spacer anchors.left: parent.left anchors.right: parent.right visible: list.listOverflowing height: 1 opacity: 0.25 color: PlasmaCore.Theme.textColor } RowLayout { id: toolLayout anchors { top: spacer.bottom right: parent.right left: parent.left leftMargin: PlasmaCore.Units.largeSpacing rightMargin: PlasmaCore.Units.largeSpacing topMargin: list.spacing bottomMargin: list.spacing } PlasmaComponents3.ToolButton { id: clearButton Layout.alignment: Qt.AlignCenter visible: hasNotifications font.bold: true font.pointSize: Kirigami.Theme.smallFont.pointSize icon.name: "edit-clear-history" text: i18n("Clear All Notifications") onClicked: clearHistory() } } } }