diff --git a/applets/notifications/package/contents/ui/NotificationItem.qml b/applets/notifications/package/contents/ui/NotificationItem.qml index 8e84e8adb..793b4597d 100644 --- a/applets/notifications/package/contents/ui/NotificationItem.qml +++ b/applets/notifications/package/contents/ui/NotificationItem.qml @@ -1,473 +1,474 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ import QtQuick 2.8 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.notificationmanager 1.0 as NotificationManager ColumnLayout { id: notificationItem property bool hovered: false property int maximumLineCount: 0 property alias bodyCursorShape: bodyLabel.cursorShape property int notificationType property bool inGroup: false property bool inHistory: false 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 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 bool showDetails 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 || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) || (jobLoader.item && jobLoader.item.menuOpen) readonly property bool dragging: (thumbnailStripLoader.item && thumbnailStripLoader.item.dragging) || (jobLoader.item && jobLoader.item.dragging) property bool replying: false signal bodyClicked(var mouse) signal closeClicked signal configureClicked signal dismissClicked signal actionInvoked(string actionName) signal replied(string text) signal openUrl(string url) signal fileActionInvoked signal suspendJobClicked signal resumeJobClicked signal killJobClicked - spacing: 0 + spacing: units.smallSpacing Item { id: headingElement Layout.fillWidth: true Layout.preferredHeight: notificationHeading.implicitHeight Layout.preferredWidth: notificationHeading.implicitWidth + Layout.bottomMargin: -parent.spacing PlasmaCore.FrameSvgItem { imagePath: "widgets/plasmoidheading" prefix: "header" anchors { fill: parent leftMargin: -margins.left rightMargin: -margins.right } visible: !notificationItem.inHistory && fromCurrentTheme } NotificationHeader { id: notificationHeading anchors { fill: parent leftMargin: notificationItem.headingLeftPadding rightMargin: notificationItem.headingRightPadding } inGroup: notificationItem.inGroup notificationType: notificationItem.notificationType jobState: notificationItem.jobState jobDetails: notificationItem.jobDetails onConfigureClicked: notificationItem.configureClicked() onDismissClicked: notificationItem.dismissClicked() onCloseClicked: notificationItem.closeClicked() } } RowLayout { id: defaultHeaderContainer Layout.fillWidth: true } // Notification body RowLayout { id: bodyRow Layout.fillWidth: true spacing: units.smallSpacing ColumnLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: summaryRow Layout.fillWidth: true visible: summaryLabel.text !== "" PlasmaExtras.Heading { id: summaryLabel Layout.fillWidth: true Layout.preferredHeight: implicitHeight textFormat: Text.PlainText maximumLineCount: 3 wrapMode: Text.WordWrap elide: Text.ElideRight level: 4 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 !== "" } // inGroup headerItem is reparented here } RowLayout { id: bodyTextRow Layout.fillWidth: true spacing: units.smallSpacing SelectableLabel { id: bodyLabel // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter Layout.fillWidth: true Layout.maximumHeight: notificationItem.maximumLineCount > 0 ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 text: notificationItem.body // Cannot do text !== "" because RichText adds some HTML tags even when empty visible: notificationItem.body !== "" onClicked: notificationItem.bodyClicked(mouse) onLinkActivated: Qt.openUrlExternally(link) } // inGroup iconContainer is reparented here } } Item { id: iconContainer Layout.preferredWidth: units.iconSizes.large Layout.preferredHeight: units.iconSizes.large Layout.topMargin: units.smallSpacing Layout.bottomMargin: units.smallSpacing visible: iconItem.active || imageItem.active PlasmaCore.IconItem { id: iconItem // don't show two identical icons readonly property bool active: valid && source != notificationItem.applicationIconSource anchors.fill: parent usesPlasmaTheme: false smooth: true source: { var icon = notificationItem.icon; if (typeof icon !== "string") { // displayed by QImageItem below return ""; } // don't show a generic "info" icon since this is a notification already if (icon === "dialog-information") { return ""; } return icon; } visible: active } KQCAddons.QImageItem { id: imageItem readonly property bool active: !null && nativeWidth > 0 anchors.fill: parent smooth: true fillMode: KQCAddons.QImageItem.PreserveAspectFit visible: active image: typeof notificationItem.icon === "object" ? notificationItem.icon : undefined } // JobItem reparents a file icon here for finished jobs with one total file } } // Job progress reporting Loader { id: jobLoader Layout.fillWidth: true 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 showDetails: notificationItem.showDetails onSuspendJobClicked: notificationItem.suspendJobClicked() onResumeJobClicked: notificationItem.resumeJobClicked() onKillJobClicked: notificationItem.killJobClicked() onOpenUrl: notificationItem.openUrl(url) onFileActionInvoked: notificationItem.fileActionInvoked() hovered: notificationItem.hovered } } Item { id: actionContainer Layout.fillWidth: true Layout.preferredHeight: Math.max(actionFlow.implicitHeight, replyLoader.height) visible: actionRepeater.count > 0 // Notification actions Flow { // it's a Flow so it can wrap if too long id: actionFlow width: parent.width spacing: units.smallSpacing layoutDirection: Qt.RightToLeft enabled: !replyLoader.active opacity: replyLoader.active ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 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; } PlasmaComponents.ToolButton { flat: false // why does it spit "cannot assign undefined to string" when a notification becomes expired? text: modelData.label || "" width: minimumWidth 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.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: units.longDuration easing.type: Easing.InOutQuad } } Behavior on opacity { NumberAnimation { duration: units.longDuration easing.type: Easing.InOutQuad } } function beginReply() { notificationItem.replying = true; plasmoid.nativeInterface.forceActivateWindow(notificationItem.Window.window); 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 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() } } 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 }*/ PropertyChanges { target: iconContainer parent: bodyTextRow } } ] }