You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

317 lines
12 KiB

/*
* Copyright 2018 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>
*/
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.notificationmanager 1.0 as NotificationManager
// TODO turn into MouseArea or MEL for "default action"
// or should that be done where the Popup/ListView is?
ColumnLayout {
id: notificationItem
property bool hovered: false
property int maximumLineCount: 0
property alias bodyCursorShape: bodyLabel.cursorShape
property int notificationType
property alias applicationIconSource: applicationIconItem.source
property alias applicationName: applicationNameLabel.text
property string summary
property var time
property alias configurable: configureButton.visible
property alias dismissable: dismissButton.visible
property alias closable: closeButton.visible
// This isn't an alias because TextEdit RichText adds some HTML tags to it
property string body
property alias icon: iconItem.source
property int jobState
property int percentage
property int error: 0
property string errorText
property bool suspendable
property bool killable
property QtObject jobDetails
property bool showDetails
property string configureActionLabel
property var actionNames: []
property var actionLabels: []
signal bodyClicked(var mouse) // TODO bodyClicked?
signal closeClicked
signal configureClicked
signal dismissClicked
signal actionInvoked(string actionName)
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
onTimeChanged: ageLabel.updateText()
spacing: units.smallSpacing
// TODO this timer should probably be at a central location
// so every notification updates simultaneously
Timer {
id: updateTimestmapTimer
interval: 60000
repeat: true
running: notificationItem.visible
&& notificationItem.Window.window
&& notificationItem.Window.window.visible
triggeredOnStart: true
onTriggered: ageLabel.updateText()
}
// Notification heading
RowLayout {
Layout.fillWidth: true
spacing: units.smallSpacing
Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, units.iconSizes.small)
PlasmaCore.IconItem {
id: applicationIconItem
Layout.preferredWidth: units.iconSizes.small
Layout.preferredHeight: units.iconSizes.small
usesPlasmaTheme: false
visible: valid
}
PlasmaExtras.DescriptiveLabel {
id: applicationNameLabel
Layout.fillWidth: true
textFormat: Text.PlainText
elide: Text.ElideRight
}
PlasmaExtras.DescriptiveLabel {
id: ageLabel
// the "n minutes ago" text, for jobs we show remaining time instead
property string agoText: ""
visible: text !== ""
text: {
if (notificationItem.notificationType === NotificationManager.Notifications.JobType
&& notificationItem.jobState !== NotificationManager.Notifications.JobStateStopped) {
var details = notificationItem.jobDetails;
if (details && details.speed > 0) {
var remaining = details.totalBytes - details.processedBytes;
if (remaining > 0) {
var eta = remaining / details.speed;
// TODO hours?
if (eta > 0 && eta < 60 * 90 /*1:30h*/) {
if (eta >= 60) {
return i18ncp("minutes remaining, keep short",
"%1min remaining", "%1min remaining",
Math.round(eta / 60));
} else {
return i18ncp("seconds remaining, keep short",
"%1s remaining", "%1s remaining", Math.round(eta));
}
}
}
}
return "";
}
return agoText;
}
function updateText() {
var time = notificationItem.time;
if (time && !isNaN(time.getTime())) {
var now = new Date();
var deltaMinutes = Math.floor((now.getTime() - time.getTime()) / 1000 / 60);
if (deltaMinutes > 0) {
agoText = i18ncp("Received minutes ago, keep short", "%1 min ago", "%1 min ago", deltaMinutes);
return;
}
}
agoText = "";
}
}
Item {
width: headerButtonsRow.width
RowLayout {
id: headerButtonsRow
spacing: units.smallSpacing * 2
anchors.verticalCenter: parent.verticalCenter
// These aren't ToolButtons so they can be perfectly aligned
// FIXME fix layout overlap
HeaderButton {
id: configureButton
tooltip: notificationItem.configureActionLabel || i18n("Configure")
iconSource: "configure"
visible: false
onClicked: notificationItem.configureClicked()
}
HeaderButton {
id: dismissButton
tooltip: i18n("Hide")
// FIXME proper icon, perhaps from widgets/configuration-icon
iconSource: "file-zoom-out"
visible: false
onClicked: notificationItem.dismissClicked()
}
HeaderButton {
id: closeButton
tooltip: i18n("Close")
iconSource: "window-close"
visible: false
onClicked: notificationItem.closeClicked()
}
}
}
}
// Notification body
RowLayout {
Layout.fillWidth: true
spacing: units.smallSpacing
ColumnLayout {
Layout.fillWidth: true
spacing: 0
PlasmaExtras.Heading {
id: summaryLabel
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
textFormat: Text.PlainText
wrapMode: Text.NoWrap
elide: Text.ElideRight
level: 4
text: {
if (notificationItem.notificationType === NotificationManager.Notifications.JobType) {
if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) {
return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary);
} else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) {
if (notificationItem.error) {
return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary);
} else {
return i18nc("Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary);
}
}
}
return notificationItem.summary;
}
// some apps use their app name as summary, avoid showing the same text twice
// try very hard to match the two
visible: text !== "" && text.toLocaleLowerCase().trim() !== applicationNameLabel.text.toLocaleLowerCase().trim()
PlasmaCore.ToolTipArea {
anchors.fill: parent
active: summaryLabel.truncated
textFormat: Text.PlainText
subText: summaryLabel.text
}
}
SelectableLabel {
id: bodyLabel
Layout.alignment: 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)
}
}
PlasmaCore.IconItem {
id: iconItem
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: units.iconSizes.large
Layout.preferredHeight: units.iconSizes.large
usesPlasmaTheme: false
smooth: true
// don't show two identical icons
visible: valid && source != applicationIconItem.source
}
}
// Job progress reporting
Loader {
Layout.fillWidth: true
active: notificationItem.notificationType === NotificationManager.Notifications.JobType
sourceComponent: JobItem {
jobState: notificationItem.jobState
error: notificationItem.error
errorText: notificationItem.errorText
percentage: notificationItem.percentage
suspendable: notificationItem.suspendable
killable: notificationItem.killable
jobDetails: notificationItem.jobDetails
showDetails: notificationItem.showDetails
onSuspendJobClicked: notificationItem.suspendJobClicked()
onResumeJobClicked: notificationItem.resumeJobClicked()
onKillJobClicked: notificationItem.killJobClicked()
hovered: notificationItem.hovered
}
}
// Notification actions
Flow { // it's a Flow so it can wrap if too long
Layout.fillWidth: true
visible: actionRepeater.count > 0
spacing: units.smallSpacing
layoutDirection: Qt.RightToLeft
Repeater {
id: actionRepeater
// HACK We want the actions to be right-aligned but Flow also reverses
// the order of items, so we manually reverse it here
model: (notificationItem.actionNames || []).reverse()
PlasmaComponents.ToolButton {
flat: false
text: notificationItem.actionLabels[actionRepeater.count - index - 1]
Layout.preferredWidth: minimumWidth
onClicked: notificationItem.actionInvoked(modelData)
}
}
}
}