widgets/notifications: Cleanup, fix some visual issues, and add jobs

Note: There was a lot of whitespace that my IDE is now removing.

Fixes job notifications not being dismissable (#208), and imports an
implementation from workspace for the progress bar and actions.

Fixes notification contents not being clipped when being dragged
(https://invent.kde.org/teams/plasma-mobile/issues/-/issues/287)

Also fixes notification text being spread over multiple lines
unnecessarily
(https://invent.kde.org/teams/plasma-mobile/issues/-/issues/302).
This commit is contained in:
Devin Lin 2024-07-26 22:29:38 -04:00
parent b426a7d59e
commit 5d84e6e47d
9 changed files with 659 additions and 160 deletions

View file

@ -6,8 +6,8 @@
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/ */
import QtQuick 2.15 import QtQuick
import QtQuick.Window 2.2 import QtQuick.Window
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents

View file

@ -35,6 +35,7 @@ Item {
NumberAnimation on dragOffset { NumberAnimation on dragOffset {
id: dragAnim id: dragAnim
duration: Kirigami.Units.longDuration duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
onFinished: { onFinished: {
if (to !== 0) { if (to !== 0) {
root.dismissRequested(); root.dismissRequested();
@ -46,16 +47,17 @@ Item {
MultiEffect { MultiEffect {
anchors.fill: mainCard anchors.fill: mainCard
visible: Math.abs(dragOffset) !== root.width visible: Math.abs(dragOffset) !== root.width
source: mainCard source: simpleShadow
blurMax: 16 blurMax: 16
shadowEnabled: true shadowEnabled: true
shadowVerticalOffset: 1 shadowVerticalOffset: 1
shadowOpacity: 0.5 shadowOpacity: 0.3
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1) shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
} }
// shadow // shadow
Rectangle { Rectangle {
id: simpleShadow
visible: Math.abs(dragOffset) !== root.width visible: Math.abs(dragOffset) !== root.width
anchors.fill: mainCard anchors.fill: mainCard
anchors.leftMargin: -1 anchors.leftMargin: -1
@ -78,7 +80,9 @@ Item {
color: (root.tapEnabled && mouseArea.pressed) ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Kirigami.Theme.backgroundColor color: (root.tapEnabled && mouseArea.pressed) ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Kirigami.Theme.backgroundColor
radius: Kirigami.Units.smallSpacing radius: Kirigami.Units.smallSpacing
implicitHeight: contentParent.implicitHeight implicitHeight: contentParent.implicitHeight
clip: true
// clip
layer.enabled: true
// ensure this is behind the content to not interfere // ensure this is behind the content to not interfere
MouseArea { MouseArea {

View file

@ -0,0 +1,66 @@
/*
SPDX-FileCopyrightText: 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.8
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
PlasmaExtras.Menu {
id: contextMenu
signal closed
property QtObject __clipboard: KQCAddons.Clipboard { }
// can be a Text or TextEdit
property Item target
property string link
onStatusChanged: {
if (status === PlasmaExtras.Menu.Closed) {
closed();
}
}
PlasmaExtras.MenuItem {
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Copy Link Address")
icon: "edit-copy-symbolic"
onClicked: __clipboard.content = contextMenu.link
visible: contextMenu.link !== ""
}
PlasmaExtras.MenuItem {
separator: true
visible: contextMenu.link !== ""
}
PlasmaExtras.MenuItem {
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Copy")
icon: "edit-copy-symbolic"
enabled: typeof target.selectionStart !== "undefined"
? target.selectionStart !== target.selectionEnd
: (target.text || "").length > 0
onClicked: {
if (typeof target.copy === "function") {
target.copy();
} else {
__clipboard.content = target.text;
}
}
}
PlasmaExtras.MenuItem {
id: selectAllAction
icon: "edit-select-all-symbolic"
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:inmenu", "Select All")
onClicked: target.selectAll()
visible: typeof target.selectAll === "function"
}
}

View file

@ -20,21 +20,11 @@ import org.kde.coreaddons 1.0 as KCoreAddons
RowLayout { RowLayout {
id: notificationHeading id: notificationHeading
property int notificationType
property var applicationIconSource property var applicationIconSource
property string applicationName property string applicationName
property string originName property string originName
property var time
property P5Support.DataSource timeSource
property int jobState
property QtObject jobDetails
property real timeout: 5000
property real remainingTime: 0
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, Kirigami.Units.iconSizes.small) Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, Kirigami.Units.iconSizes.small)

View file

@ -40,13 +40,6 @@ BaseNotificationItem {
applicationName: notificationItem.applicationName applicationName: notificationItem.applicationName
applicationIconSource: notificationItem.applicationIconSource applicationIconSource: notificationItem.applicationIconSource
originName: notificationItem.originName originName: notificationItem.originName
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
jobDetails: notificationItem.jobDetails
time: notificationItem.time
timeSource: notificationItem.timeSource
} }
// notification // notification
@ -59,7 +52,7 @@ BaseNotificationItem {
tapEnabled: notificationItem.hasDefaultAction tapEnabled: notificationItem.hasDefaultAction
onTapped: notificationItem.actionInvoked("default"); onTapped: notificationItem.actionInvoked("default");
swipeGestureEnabled: notificationItem.notificationType != NotificationManager.Notifications.JobType swipeGestureEnabled: notificationItem.closable
onDismissRequested: notificationItem.close() onDismissRequested: notificationItem.close()
ColumnLayout { ColumnLayout {
@ -105,15 +98,9 @@ BaseNotificationItem {
NotificationBodyLabel { NotificationBodyLabel {
id: bodyLabel id: bodyLabel
Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.fillWidth: true Layout.preferredWidth: column.width - iconContainer.width - Kirigami.Units.smallSpacing
// HACK RichText does not allow to specify link color and since LineEdit text: notificationItem.body
// does not support StyledText, we have to inject some CSS to force the color,
// cf. QTBUG-81463 and to some extent QTBUG-80354
text: "<style>a { color: " + Kirigami.Theme.linkColor + "; }</style>" + notificationItem.body
// Cannot do text !== "" because RichText adds some HTML tags even when empty
visible: notificationItem.body !== ""
} }
// notification icon // notification icon
@ -139,9 +126,37 @@ BaseNotificationItem {
} }
} }
// Job progress reporting
Loader {
id: jobLoader
Layout.fillWidth: true
Layout.preferredHeight: item ? item.implicitHeight : 0
active: notificationItem.notificationType === NotificationManager.Notifications.JobType
visible: active
sourceComponent: NotificationJobItem {
iconContainerItem: iconContainer
jobState: notificationItem.jobState
jobError: notificationItem.jobError
percentage: notificationItem.percentage
suspendable: notificationItem.suspendable
killable: notificationItem.killable
jobDetails: notificationItem.jobDetails
onSuspendJobClicked: notificationItem.suspendJobClicked()
onResumeJobClicked: notificationItem.resumeJobClicked()
onKillJobClicked: notificationItem.killJobClicked()
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked(action)
}
}
// notification actions // notification actions
NotificationFooterActions { NotificationFooterActions {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
notification: notificationItem notification: notificationItem
} }

View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.8
import QtQuick.Layouts 1.1
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kirigami 2.20 as Kirigami
import org.kde.coreaddons 1.0 as KCoreAddons
GridLayout {
id: detailsGrid
property QtObject jobDetails
columns: 2
rowSpacing: Math.round(Kirigami.Units.smallSpacing / 2)
columnSpacing: Kirigami.Units.smallSpacing
// once you use Layout.column/Layout.row *all* of the items in the Layout have to use them
Repeater {
model: [1, 2]
PlasmaExtras.DescriptiveLabel {
Layout.column: 0
Layout.row: index
Layout.alignment: Qt.AlignTop | Qt.AlignRight
text: jobDetails["descriptionLabel" + modelData] && jobDetails["descriptionValue" + modelData]
? i18ndc("plasma_applet_org.kde.plasma.notifications", "Row description, e.g. Source", "%1:", jobDetails["descriptionLabel" + modelData]) : ""
font: Kirigami.Theme.smallFont
textFormat: Text.PlainText
visible: text !== ""
}
}
Repeater {
model: [1, 2]
PlasmaExtras.DescriptiveLabel {
id: descriptionValueLabel
Layout.column: 1
Layout.row: index
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
elide: Text.ElideMiddle
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
verticalAlignment: Text.AlignTop
maximumLineCount: 5
visible: text !== ""
// Only let the label grow, never shrink, to avoid repeatedly resizing the dialog when copying many files
onImplicitHeightChanged: {
if (implicitHeight > Layout.preferredHeight) {
Layout.preferredHeight = implicitHeight;
}
}
Component.onCompleted: bindText()
function bindText() {
text = Qt.binding(function() {
return jobDetails["descriptionValue" + modelData] || "";
});
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: mouse => {
// break binding so it doesn't update while the menu is opened
descriptionValueLabel.text = descriptionValueLabel.text;
descriptionValueMenu.open(mouse.x, mouse.y)
}
}
NotificationEditContextMenu {
id: descriptionValueMenu
target: descriptionValueLabel
// defer re-binding until after the "Copy" action in the menu has triggered
onClosed: Qt.callLater(descriptionValueLabel.bindText)
}
}
}
Repeater {
model: ["Bytes", "Files", "Directories", "Items"]
PlasmaExtras.DescriptiveLabel {
Layout.column: 1
Layout.row: 2 + index
Layout.fillWidth: true
text: {
var processed = jobDetails["processed" + modelData];
var total = jobDetails["total" + modelData];
if (processed > 0 || total > 1) {
if (processed > 0 && total > 0 && processed <= total) {
switch(modelData) {
case "Bytes":
return i18ndc("plasma_applet_org.kde.plasma.notifications", "How many bytes have been copied", "%2 of %1",
KCoreAddons.Format.formatByteSize(total),
KCoreAddons.Format.formatByteSize(processed))
case "Files":
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many files have been copied", "%2 of %1 file", "%2 of %1 files",
total, processed);
case "Directories":
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many dirs have been copied", "%2 of %1 folder", "%2 of %1 folders",
total, processed);
case "Items":
return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many items (that includes files and dirs) have been copied", "%2 of %1 item", "%2 of %1 items",
total, processed);
}
} else {
switch(modelData) {
case "Bytes":
return KCoreAddons.Format.formatByteSize(processed || total)
case "Files":
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 file", "%1 files", (processed || total));
case "Directories":
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 folder", "%1 folders", (processed || total));
case "Items":
return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 item", "%1 items", (processed || total));
}
}
}
return "";
}
font: Kirigami.Theme.smallFont
textFormat: Text.PlainText
visible: text !== ""
}
}
PlasmaExtras.DescriptiveLabel {
Layout.column: 1
Layout.row: 2 + 4
Layout.fillWidth: true
text: jobDetails.speed > 0 ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Bytes per second", "%1/s",
KCoreAddons.Format.formatByteSize(jobDetails.speed)) : ""
font: Kirigami.Theme.smallFont
textFormat: Text.PlainText
visible: text !== ""
}
}

View file

@ -0,0 +1,277 @@
// SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
// SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
import QtQml 2.15
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.kirigami 2.20 as Kirigami
import org.kde.notificationmanager as NotificationManager
import org.kde.plasma.private.notifications 2.0 as Notifications
ColumnLayout {
id: jobItem
property int jobState
property int jobError
property alias percentage: progressBar.value
property alias suspendable: suspendButton.visible
property alias killable: killButton.visible
property QtObject jobDetails
readonly property int totalFiles: jobItem.jobDetails && jobItem.jobDetails.totalFiles || 0
readonly property var url: {
if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped
|| jobItem.jobError) {
return null;
}
// For a single file show actions for it
// Otherwise the destination folder all of them were copied into
const url = totalFiles === 1 ? jobItem.jobDetails.descriptionUrl
: jobItem.jobDetails.destUrl;
// Don't offer opening files in Trash
if (url && url.toString().startsWith("trash:")) {
return null;
}
return url;
}
property alias iconContainerItem: busyIndicatorContainer.parent
readonly property alias menuOpen: otherFileActionsMenu.visible
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
signal openUrl(string url)
signal fileActionInvoked(QtObject action)
spacing: Kirigami.Units.smallSpacing
Notifications.FileInfo {
id: fileInfo
url: jobItem.totalFiles === 1 && jobItem.url ? jobItem.url : ""
}
Item {
id: busyIndicatorContainer
width: parent ? parent.width : 0
height: parent ? parent.height : 0
PlasmaComponents3.BusyIndicator {
id: busyIndicator
anchors.centerIn: parent
running: fileInfo.busy && !delayBusyTimer.running
visible: running
// Avoid briefly flashing the busy indicator
Timer {
id: delayBusyTimer
interval: 500
repeat: false
running: fileInfo.busy
}
}
}
RowLayout {
id: progressRow
Layout.fillWidth: true
// Even when indeterminate, we want to reserve the height for the text, otherwise it's too tightly spaced
Layout.minimumHeight: progressText.implicitHeight
// We want largeSpacing between the progress bar and the label
spacing: Kirigami.Units.largeSpacing
PlasmaComponents3.ProgressBar {
id: progressBar
Layout.fillWidth: true
from: 0
to: 100
// TODO do we actually need the window visible check? perhaps I do because it can be in popup or expanded plasmoid
indeterminate: visible && Window.window && Window.window.visible && percentage < 1
&& jobItem.jobState === NotificationManager.Notifications.JobStateRunning
// is this too annoying?
&& (jobItem.jobDetails.processedBytes === 0 || jobItem.jobDetails.totalBytes === 0)
&& jobItem.jobDetails.processedFiles === 0
//&& jobItem.jobDetails.processedDirectories === 0
}
PlasmaComponents3.Label {
id: progressText
visible: !progressBar.indeterminate
// the || "0" is a workaround for the fact that 0 as number is falsey, and is wrongly considered a missing argument
// BUG: 451807
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Percentage of a job", "%1%", jobItem.percentage || "0")
textFormat: Text.PlainText
}
}
RowLayout {
id: jobActionsRow
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents3.Button {
id: expandButton
icon.name: checked ? "collapse-symbolic" : "expand-symbolic"
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Hides/expands item details", "Details")
checkable: true
enabled: jobItem.jobDetails && jobItem.jobDetails.hasDetails
}
Item { Layout.fillWidth: true }
PlasmaComponents3.Button {
id: suspendButton
icon.name: "media-playback-pause-symbolic"
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Pause running job", "Pause")
onClicked: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended ? jobItem.resumeJobClicked()
: jobItem.suspendJobClicked()
}
PlasmaComponents3.Button {
id: killButton
icon.name: "dialog-cancel-symbolic"
text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Cancel running job", "Cancel")
onClicked: jobItem.killJobClicked()
}
}
Loader {
Layout.fillWidth: true
Layout.preferredHeight: item ? item.implicitHeight : 0
active: expandButton.checked
// Loader doesn't reset its height when unloaded, just hide it altogether
visible: active
sourceComponent: NotificationJobDetails {
jobDetails: jobItem.jobDetails
}
}
Row {
id: fileActionsRow
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
// We want the actions to be right-aligned but Row also reverses
// the order of items, so we put them in reverse order
layoutDirection: Qt.RightToLeft
visible: jobItem.url && jobItem.url.toString() !== "" && !fileInfo.error
PlasmaComponents3.Button {
id: otherFileActionsButton
height: Math.max(implicitHeight, openButton.implicitHeight)
icon.name: "application-menu-symbolic"
checkable: true
text: openButton.visible ? "" : Accessible.name
Accessible.name: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options…")
onPressedChanged: {
if (pressed) {
checked = Qt.binding(function() {
return otherFileActionsMenu.visible;
});
otherFileActionsMenu.visualParent = this;
// -1 tells it to "align bottom left of visualParent (this)"
otherFileActionsMenu.open(-1, -1);
}
}
PlasmaComponents3.ToolTip {
text: parent.Accessible.name
enabled: parent.text === ""
}
Notifications.FileMenu {
id: otherFileActionsMenu
url: jobItem.url || ""
onActionTriggered: jobItem.fileActionInvoked(action)
}
}
PlasmaComponents3.Button {
id: openButton
width: Math.min(implicitWidth, jobItem.width - otherFileActionsButton.width - fileActionsRow.spacing)
height: Math.max(implicitHeight, otherFileActionsButton.implicitHeight)
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open")
onClicked: jobItem.openUrl(jobItem.url)
states: [
State {
when: jobItem.jobDetails && jobItem.jobDetails.totalFiles !== 1
PropertyChanges {
target: openButton
text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open Containing Folder")
icon.name: "folder-open-symbolic"
}
},
State {
when: fileInfo.openAction
PropertyChanges {
target: openButton
text: fileInfo.openAction.text
icon.name: fileInfo.openActionIconName
visible: fileInfo.openAction.enabled
onClicked: {
fileInfo.openAction.trigger();
jobItem.fileActionInvoked(fileInfo.openAction);
}
}
}
]
}
}
states: [
State {
when: jobItem.jobState === NotificationManager.Notifications.JobStateRunning
PropertyChanges {
target: suspendButton
// Explicitly set it to false so it unchecks when pausing from applet
// and then the job unpauses programmatically elsewhere.
checked: false
}
},
State {
when: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended
PropertyChanges {
target: suspendButton
checked: true
}
PropertyChanges {
target: progressBar
enabled: false
}
},
State {
when: jobItem.jobState === NotificationManager.Notifications.JobStateStopped
PropertyChanges {
target: progressRow
visible: false
}
PropertyChanges {
target: jobActionsRow
visible: false
}
PropertyChanges {
target: expandButton
checked: false
}
}
]
}

View file

@ -254,7 +254,6 @@ Item {
applicationName: model.applicationName applicationName: model.applicationName
applicationIconSource: model.applicationIconName applicationIconSource: model.applicationIconName
originName: model.originName || "" originName: model.originName || ""
timeSource: timeDataSource
} }
} }