shift-shell/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

446 lines
16 KiB
QML
Raw Normal View History

/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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
2023-03-03 05:48:24 +00:00
import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.notificationmanager as NotificationManager
/**
* Embeddable notification list widget optimized for mobile and touch.
* Used on the lockscreen and action drawer.
*/
Item {
id: root
/**
* If the notification is in the lockscreen.
*/
property bool inLockScreen: false
/**
* The panel background type for all the notifications within the widget.
*/
property int panelType: MobileShell.PanelBackground.PanelType.Drawer
/**
* The color scheme of the foreground cards (but not notification headers, which are outside of it).
*/
property var cardColorScheme: Kirigami.Theme.View
/**
* 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
/**
* Top padding of the notification list.
*/
property int topPadding: 0
/**
* Bottom padding of the notification list.
*/
property int bottomPadding: 0
/**
* Header component for notification list.
*/
property var header
/**
* Whether to show the header component.
*/
property bool showHeader: false
/**
* Gives access to the notification list view outside of the notification widget.
*/
property alias listView: list
/**
* 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()
/**
* Run pending action that was pending for authentication when unlockRequested() was emitted.
*/
function runPendingAction() {
if (list.pendingNotificationWithAction) {
list.pendingNotificationWithAction.runPendingAction();
list.pendingNotificationWithAction = null;
}
}
/**
* Clears the history of the notification model.
*/
function clearHistory() {
historyModel.clear(NotificationManager.Notifications.ClearExpired);
if (historyModel.count === 0) {
backgroundClicked();
}
}
/**
* Sets Do Not Disturb mode to the intended setting.
* Note: The state may not change to the desired setting, always read doNotDisturbEnabled for the current state.
*/
function setDoNotDisturbMode(doNotDisturb: bool) {
if (!doNotDisturb) {
// Turn off do not disturb
notificationSettings.notificationsInhibitedUntil = undefined;
notificationSettings.revokeApplicationInhibitions();
notificationSettings.fullscreenFocused = false;
// overrules current mirrored screen setup, updates again when screen configuration changes
notificationSettings.screensMirrored = false;
} else {
// Turn on do not disturb
// We just have a global toggle, so set it to a really long time (in this case, a year)
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");
}
// Implement listening to system "do not disturb" requests
Connections {
target: MobileShellState.ShellDBusClient
function onDoNotDisturbChanged() {
if (root.doNotDisturbModeEnabled !== MobileShellState.ShellDBusClient.doNotDisturb) {
root.setDoNotDisturbMode(MobileShellState.ShellDBusClient.doNotDisturb);
}
}
}
P5Support.DataSource {
id: timeDataSource
engine: "time"
connectedSources: ["Local"]
interval: 60000 // 1 min
2023-03-09 15:45:30 +00:00
intervalAlignment: P5Support.Types.AlignToMinute
}
ListView {
id: list
model: historyModel
clip: true
currentIndex: 0
property NotificationItem pendingNotificationWithAction: null
readonly property int animationDuration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.longDuration : 0
// If a screen overflow occurs, fix height in order to maintain tool buttons in place.
readonly property bool listOverflowing: listHeight + spacing >= root.height
readonly property int listHeight: contentItem.childrenRect.height
bottomMargin: spacing
height: count === 0 ? (root.topPadding + (showHeader ? root.header.height + listHeight + Kirigami.Units.largeSpacing * 2 : 0)) : (listOverflowing ? root.height : listHeight + bottomMargin)
anchors {
top: parent.top
left: parent.left
right: parent.right
}
boundsBehavior: Flickable.DragAndOvershootBounds
spacing: Kirigami.Units.largeSpacing
// TODO keyboard focus
highlightMoveDuration: 0
highlightResizeDuration: 0
highlight: Item {}
// media control widget
// added to the notification list when in landscape mode
Component {
id: headerComponent
Item {
width: parent.width
MobileShell.BaseItem {
id: headerComponentProxy
contentItem: showHeader ? root.header : null
y: root.topPadding + Kirigami.Units.largeSpacing
width: parent.width - Kirigami.Units.gridUnit * 2
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.gridUnit
}
}
}
// set bottom padding for the notification list
Component {
id: footerComponent
Item {
width: parent.width
height: root.bottomPadding
}
}
header: headerComponent
footer: footerComponent
section {
property: "isGroup"
criteria: ViewSection.FullString
}
PlasmaExtras.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.gridUnit * 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: Kirigami.Units.gridUnit
right: parent ? parent.right : undefined
rightMargin: Kirigami.Units.gridUnit
}
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 }
}
// adjust top paddding for media control widget
Component {
id: groupDelegate
Column {
spacing: Kirigami.Units.smallSpacing
height: headerSpace.height + groupHeader.height
Item {
id: headerSpace
width: parent.width
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
visible: index == 0
}
NotificationGroupHeader {
id: groupHeader
applicationName: model.applicationName
applicationIconSource: model.applicationIconName
originName: model.originName || ""
}
}
}
Component {
id: notificationDelegate
Column {
spacing: Kirigami.Units.smallSpacing
height: headerSpace.height + notificationItem.height + showMoreLoader.height
Item {
id: headerSpace
width: parent.width
height: index == 0 ? root.topPadding + (showHeader && root.header.visible ? root.header.height + Kirigami.Units.largeSpacing * 2 : 0) : 0
visible: index == 0
}
NotificationItem {
id: notificationItem
width: parent.width
height: implicitHeight
inLockScreen: root.inLockScreen
panelType: root.panelType
cardColorScheme: root.cardColorScheme
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)
}
}
}
}
}
}
}
}