aerothemeplasma/plasma/plasmoids/org.kde.plasma.notifications/contents/ui/NotificationItem.qml
2024-08-09 03:20:25 +02:00

473 lines
19 KiB
QML

/*
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.8
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
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: notificationItem
property int maximumLineCount: 0
property alias bodyCursorShape: bodyLabel.cursorShape
property int notificationType
property int urgency
property bool inGroup: false
property bool inHistory: false
property ListView listViewParent: null
property alias applicationIconSource: notificationHeading.applicationIconSource
property alias applicationName: notificationHeading.applicationName
property alias originName: notificationHeading.originName
property string summary
property alias time: notificationHeading.time
property alias configurable: notificationHeading.configurable
property alias dismissable: notificationHeading.dismissable
property alias dismissed: notificationHeading.dismissed
property alias closable: notificationHeading.closable
// This isn't an alias because TextEdit RichText adds some HTML tags to it
property string body
property string accessibleDescription
property var icon
property var urls: []
property int jobState
property int percentage
property int jobError: 0
property bool suspendable
property bool killable
property QtObject jobDetails
property alias configureActionLabel: notificationHeading.configureActionLabel
property var actionNames: []
property var actionLabels: []
property bool hasReplyAction
property string replyActionLabel
property string replyPlaceholderText
property string replySubmitButtonText
property string replySubmitButtonIconName
property int headingLeftPadding: 0
property int headingRightPadding: 0
property int thumbnailLeftPadding: 0
property int thumbnailRightPadding: 0
property int thumbnailTopPadding: 0
property int thumbnailBottomPadding: 0
property alias timeout: notificationHeading.timeout
property alias remainingTime: notificationHeading.remainingTime
readonly property bool menuOpen: bodyLabel.contextMenu !== null
|| Boolean(thumbnailStripLoader.item?.menuOpen || jobLoader.item?.menuOpen)
readonly property bool dragging: Boolean(thumbnailStripLoader.item?.dragging || jobLoader.item?.dragging)
property bool replying: false
readonly property bool hasPendingReply: replyLoader.item?.text !== ""
readonly property alias headerHeight: headingElement.height
property int extraSpaceForCriticalNotificationLine: 0
signal bodyClicked
signal closeClicked
signal configureClicked
signal dismissClicked
signal actionInvoked(string actionName)
signal replied(string text)
signal openUrl(string url)
signal fileActionInvoked(QtObject action)
signal forceActiveFocusRequested
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
spacing: 0//Kirigami.Units.smallSpacing
Kirigami.Theme.colorSet: notificationItem.inHistory ? Kirigami.Theme.Header : Kirigami.Theme.View
Kirigami.Theme.inherit: false
// Header
Item {
id: headingElement
Layout.fillWidth: !notificationItem.inGroup
Layout.preferredHeight: notificationHeading.implicitHeight
Layout.preferredWidth: notificationHeading.implicitWidth
Layout.alignment: notificationItem.inGroup && summaryLabel.lineCount > 1 ? Qt.AlignTop : 0
Layout.topMargin: notificationItem.inGroup && summaryLabel.lineCount > 1 ? Math.max(0, (summaryLabelTextMetrics.height - Layout.preferredHeight) / 2) : Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
//Layout.bottomMargin: notificationItem.inGroup ? 0 : -parent.spacing
Layout.rightMargin: Kirigami.Units.smallSpacing/2
/*PlasmaExtras.PlasmoidHeading {
topInset: 0
anchors.fill: parent
visible: !notificationItem.inHistory
opacity: 0
// HACK PlasmoidHeading is a QQC2 Control which accepts left mouse button by default,
// which breaks the popup default action mouse handler, cf. QTBUG-89785
Component.onCompleted: Notifications.InputDisabler.makeTransparentForInput(this)
}*/
NotificationHeader {
id: notificationHeading
anchors {
fill: parent
leftMargin: notificationItem.headingLeftPadding
rightMargin: notificationItem.headingRightPadding
}
inGroup: notificationItem.inGroup
inHistory: notificationItem.inHistory
urgency: notificationItem.urgency
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
jobDetails: notificationItem.jobDetails
onConfigureClicked: notificationItem.configureClicked()
onDismissClicked: notificationItem.dismissClicked()
onCloseClicked: notificationItem.closeClicked()
}
}
// Everything else that goes below the header
// This is its own ColumnLayout-within-a-ColumnLayout because it lets us set
// the left margin once rather than several times, in each of its children
Item {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
Layout.leftMargin: (notificationItem.inGroup || !notificationItem.inHistory ? 0 : notificationItem.spacing) + notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
Layout.topMargin: notificationItem.inHistory ? 0 : -Kirigami.Units.smallSpacing
Accessible.role: notificationItem.inHistory ? Accessible.NoRole : Accessible.Notification
Accessible.name: summaryLabel.text
Accessible.description: notificationItem.accessibleDescription
// Notification body
RowLayout {
id: summaryRow
anchors {
left: parent.left
right: notificationItem.inGroup ? parent.right : iconContainer.left
rightMargin: notificationItem.inGroup ? 0 : notificationItem.spacing
}
visible: summaryLabel.text !== ""
Kirigami.Heading {
id: summaryLabel
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Layout.topMargin: notificationItem.inGroup && lineCount > 1 ? Math.max(0, (headingElement.Layout.preferredHeight - summaryLabelTextMetrics.height) / 2) : 0
Layout.bottomMargin: Kirigami.Units.smallSpacing
textFormat: Text.PlainText
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
opacity: 0.66
level: 5
color: "black"
// Give it a bit more visual prominence than the app name in the header
//type: Kirigami.Heading.Type.Primary
text: {
if (notificationItem.notificationType === NotificationManager.Notifications.JobType) {
if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) {
if (notificationItem.summary) {
return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary);
}
} else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) {
if (notificationItem.jobError) {
if (notificationItem.summary) {
return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary);
} else {
return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Failed");
}
} else {
if (notificationItem.summary) {
return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary);
} else {
return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Finished");
}
}
}
}
// some apps use their app name as summary, avoid showing the same text twice
// try very hard to match the two
if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() != notificationItem.applicationName.toLocaleLowerCase().trim()) {
return notificationItem.summary;
}
return "";
}
visible: text !== ""
TextMetrics {
id: summaryLabelTextMetrics
font: summaryLabel.font
text: summaryLabel.text
}
}
// inGroup headerItem is reparented here
}
SelectableLabel {
id: bodyLabel
readonly property real maximumHeight: Kirigami.Units.iconSizes.small * notificationItem.maximumLineCount
readonly property bool truncated: notificationItem.maximumLineCount > 0 && bodyLabel.implicitHeight > maximumHeight
height: truncated ? maximumHeight : implicitHeight
anchors {
top: summaryRow.bottom
topMargin: summaryRow.visible && notificationItem.inGroup && iconContainer.visible ? notificationItem.spacing : 0
left: parent.left
right: iconContainer.left
rightMargin: iconContainer.visible ? notificationItem.spacing : 0
}
listViewParent: notificationItem.listViewParent
// HACK RichText does not allow to specify link color and since LineEdit
// 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 !== ""
onClicked: notificationItem.bodyClicked()
onLinkActivated: Qt.openUrlExternally(link)
}
Item {
id: iconContainer
width: visible ? iconItem.width : 0
height: visible ? Math.max(iconItem.height + notificationItem.spacing * 2, bodyLabel.height + bodyLabel.anchors.topMargin + (notificationItem.inGroup ? 0 : summaryRow.implicitHeight)) : 0
anchors {
top: notificationItem.inGroup ? bodyLabel.top : parent.top
right: parent.right
}
visible: iconItem.shouldBeShown
Kirigami.Icon {
id: iconItem
width: Kirigami.Units.iconSizes.large
height: Kirigami.Units.iconSizes.large
anchors.verticalCenter: parent.verticalCenter
// don't show two identical icons
readonly property bool shouldBeShown: valid && source != notificationItem.applicationIconSource
smooth: true
source: notificationItem.icon
}
}
}
// Job progress reporting
Loader {
id: jobLoader
Layout.fillWidth: true
Layout.preferredHeight: item ? item.implicitHeight : 0
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
active: notificationItem.notificationType === NotificationManager.Notifications.JobType
visible: active
sourceComponent: JobItem {
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)
}
}
// Actions
Item {
id: actionContainer
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: -Kirigami.Units.smallSpacing
Layout.leftMargin: notificationHeading.hasIcon ? (Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing) : 0
visible: actionRepeater.count > 0 && actionFlow.parent === this
// Notification actions
Flow { // it's a Flow so it can wrap if too long
id: actionFlow
// For a cleaner look, if there is a thumbnail, puts the actions next to the thumbnail strip's menu button
parent: thumbnailStripLoader.item?.actionContainer ?? actionContainer
width: parent.width
spacing: Kirigami.Units.smallSpacing
layoutDirection: Qt.RightToLeft
enabled: !replyLoader.active
opacity: replyLoader.active ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
Repeater {
id: actionRepeater
model: {
var buttons = [];
var actionNames = (notificationItem.actionNames || []);
var actionLabels = (notificationItem.actionLabels || []);
// HACK We want the actions to be right-aligned but Flow also reverses
for (var i = actionNames.length - 1; i >= 0; --i) {
buttons.push({
actionName: actionNames[i],
label: actionLabels[i]
});
}
if (notificationItem.hasReplyAction) {
buttons.unshift({
actionName: "inline-reply",
label: notificationItem.replyActionLabel || i18nc("Reply to message", "Reply")
});
}
return buttons;
}
PlasmaComponents3.ToolButton {
flat: false
// why does it spit "cannot assign undefined to string" when a notification becomes expired?
text: modelData.label || ""
onClicked: {
if (modelData.actionName === "inline-reply") {
replyLoader.beginReply();
return;
}
notificationItem.actionInvoked(modelData.actionName);
}
}
}
}
// inline reply field
Loader {
id: replyLoader
width: parent.width
height: active && item ? item.implicitHeight : 0
// When there is only one action and it is a reply action, show text field right away
active: notificationItem.replying || (notificationItem.hasReplyAction && (notificationItem.actionNames || []).length === 0)
visible: active
opacity: active ? 1 : 0
x: active ? 0 : parent.width
/*Behavior on x {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}*/
function beginReply() {
notificationItem.replying = true;
notificationItem.forceActiveFocusRequested();
replyLoader.item.activate();
}
sourceComponent: NotificationReplyField {
placeholderText: notificationItem.replyPlaceholderText
buttonIconName: notificationItem.replySubmitButtonIconName
buttonText: notificationItem.replySubmitButtonText
onReplied: notificationItem.replied(text)
replying: notificationItem.replying
onBeginReplyRequested: replyLoader.beginReply()
}
}
}
// Thumbnails
Loader {
id: thumbnailStripLoader
Layout.leftMargin: notificationItem.thumbnailLeftPadding
Layout.rightMargin: notificationItem.thumbnailRightPadding
// no change in Layout.topMargin to keep spacing to notification text consistent
Layout.topMargin: 0
Layout.bottomMargin: notificationItem.thumbnailBottomPadding
Layout.fillWidth: true
Layout.preferredHeight: item ? item.implicitHeight : 0
active: notificationItem.urls.length > 0
visible: active
sourceComponent: ThumbnailStrip {
leftPadding: -thumbnailStripLoader.Layout.leftMargin
rightPadding: -thumbnailStripLoader.Layout.rightMargin
topPadding: -notificationItem.thumbnailTopPadding
bottomPadding: -thumbnailStripLoader.Layout.bottomMargin
urls: notificationItem.urls
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked(action)
}
}
states: [
State {
when: notificationItem.inGroup
PropertyChanges {
target: headingElement
parent: summaryRow
}
PropertyChanges {
target: summaryRow
visible: true
}
PropertyChanges {
target: summaryLabel
visible: true
}
/*PropertyChanges {
target: bodyLabel.Label
alignment: Qt.AlignTop
}*/
}
]
}