mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
388 lines
15 KiB
QML
388 lines
15 KiB
QML
/*
|
|
* 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
|
|
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: P5Support.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()
|
|
}
|
|
}
|
|
}
|
|
}
|