diff --git a/applets/notifications/package/contents/ui/JobDelegate.qml b/applets/notifications/package/contents/ui/JobDelegate.qml index 2f803ef55..157214be0 100644 --- a/applets/notifications/package/contents/ui/JobDelegate.qml +++ b/applets/notifications/package/contents/ui/JobDelegate.qml @@ -1,190 +1,192 @@ /* * Copyright 2011 Marco Martin * Copyright 2014 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 Column { id: jobItem width: parent.width spacing: jobItem.layoutSpacing readonly property int layoutSpacing: units.largeSpacing / 4 readonly property int animationDuration: units.shortDuration * 2 readonly property string infoMessage: getData(jobsSource.data, "infoMessage", '') readonly property string labelName0: getData(jobsSource.data, "labelName0", '') readonly property string labelName1: getData(jobsSource.data, "labelName1", '') readonly property string label0: getData(jobsSource.data, "label0", '') readonly property string label1: getData(jobsSource.data, "label1", '') readonly property bool isSuspended: getData(jobsSource.data, "state", '') === "suspended" function getData(data, name, defaultValue) { var source = model.name return data[source] ? (data[source][name] ? data[source][name] : defaultValue) : defaultValue; } PlasmaExtras.Heading { id: infoLabel width: parent.width opacity: 0.6 level: 3 text: jobItem.isSuspended ? i18nc("Placeholder is job name, eg. 'Copying'", "%1 (Paused)", infoMessage) : infoMessage + textFormat: Text.PlainText } RowLayout { width: parent.width PlasmaComponents.Label { id: summary Layout.fillWidth: true elide: Text.ElideMiddle text: { var labelSplit = label0.split("/") return labelSplit[labelSplit.length-1] } + textFormat: Text.PlainText } PlasmaComponents.ToolButton { id: expandButton iconSource: checked ? "arrow-down" : (LayoutMirroring.enabled ? "arrow-left" : "arrow-right") tooltip: checked ? i18nc("A button tooltip; hides item details", "Hide Details") : i18nc("A button tooltip; expands the item to show details", "Show Details") checkable: true onCheckedChanged: { if (checked) { // Need to force the Loader active here, otherwise the transition // doesn't fire because the height is still 0 without a loaded item detailsLoader.active = true } } } } Loader { id: detailsLoader width: parent.width height: 0 //visible: false // this breaks the opening transition but given the loaded item is released anyway... source: "JobDetailsItem.qml" active: false opacity: state === "expanded" ? 0.6 : 0 Behavior on opacity { NumberAnimation { duration: jobItem.animationDuration } } states: [ State { name: "expanded" when: expandButton.checked && detailsLoader.status === Loader.Ready PropertyChanges { target: detailsLoader height: detailsLoader.item.implicitHeight } } ] transitions : [ Transition { from: "" // default state - collapsed to: "expanded" SequentialAnimation { ScriptAction { script: detailsLoader.clip = true } NumberAnimation { duration: jobItem.animationDuration properties: "height" easing.type: Easing.InOutQuad } ScriptAction { script: detailsLoader.clip = false } } }, Transition { from: "expanded" to: "" // default state - collapsed SequentialAnimation { ScriptAction { script: detailsLoader.clip = true } NumberAnimation { duration: jobItem.animationDuration properties: "height" easing.type: Easing.InOutQuad } ScriptAction { script: { detailsLoader.clip = false detailsLoader.active = false } } } } ] } RowLayout { width: parent.width height: pauseButton.height spacing: jobItem.layoutSpacing PlasmaComponents.ProgressBar { id: progressBar Layout.fillWidth: true //height: units.gridUnit minimumValue: 0 maximumValue: 100 //percentage doesn't always exist, so doesn't get in the model value: getData(jobsSource.data, "percentage", 0) indeterminate: plasmoid.expanded && jobsSource.data[model.name] && typeof jobsSource.data[model.name]["percentage"] === "undefined" && !jobItem.isSuspended } PlasmaComponents.ToolButton { id: pauseButton iconSource: jobItem.isSuspended ? "media-playback-start" : "media-playback-pause" visible: getData(jobsSource.data, "suspendable", 0) onClicked: { var operationName = "suspend" if (jobItem.isSuspended) { operationName = "resume" } var service = jobsSource.serviceForSource(model.name) var operation = service.operationDescription(operationName) service.startOperationCall(operation) } } PlasmaComponents.ToolButton { id: stopButton iconSource: "media-playback-stop" visible: getData(jobsSource.data, "killable", 0) onClicked: { var service = jobsSource.serviceForSource(model.name) var operation = service.operationDescription("stop") service.startOperationCall(operation) } } } } diff --git a/applets/notifications/package/contents/ui/JobDetailsItem.qml b/applets/notifications/package/contents/ui/JobDetailsItem.qml index 9e25acb43..f278c8f4d 100644 --- a/applets/notifications/package/contents/ui/JobDetailsItem.qml +++ b/applets/notifications/package/contents/ui/JobDetailsItem.qml @@ -1,147 +1,152 @@ /* * Copyright 2011 Marco Martin * Copyright 2014 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.kcoreaddons 1.0 as KCoreAddons import org.kde.kquickcontrolsaddons 2.0 // Unfortunately ColumnLayout was a pain to use, so it's a Column with Rows inside Column { id: detailsItem spacing: jobItem.layoutSpacing readonly property int eta: jobItem.getData(jobsSource.data, "eta", 0) readonly property string speed: jobItem.getData(jobsSource.data, "speed", '') property int leftColumnWidth function localizeProcessedAmount(id) { var data = jobsSource.data[modelData] if (!data) { return "" } var unit = data["processedUnit" + id] var processed = data["processedAmount" + id] var total = data["totalAmount" + id] //if bytes localise the unit if (unit === "bytes") { return i18nc("How much many bytes (or whether unit in the locale has been copied over total", "%1 of %2", KCoreAddons.Format.formatByteSize(processed), KCoreAddons.Format.formatByteSize(total)) //else print something only if is interesting data (ie more than one file/directory etc to copy } else if (total > 1) { // HACK Actually the owner of the job is responsible for sending the unit in a user-displayable // way but this has been broken for years and every other unit (other than files and dirs) is correct if (unit === "files") { return i18ncp("Either just 1 file or m of n files are being processed", "1 file", "%2 of %1 files", total, processed) } else if (unit === "dirs") { return i18ncp("Either just 1 dir or m of n dirs are being processed", "1 dir", "%2 of %1 dirs", total, processed) } return i18n("%1 of %2 %3", processed, total, unit) } else { return "" } } // The 2 main labels (eg. Source and Destination) Repeater { model: 2 RowLayout { width: parent.width spacing: jobItem.layoutSpacing visible: labelNameText.text !== "" || labelText.text !== "" PlasmaComponents.Label { id: labelNameText Layout.minimumWidth: leftColumnWidth Layout.maximumWidth: leftColumnWidth height: paintedHeight onPaintedWidthChanged: { if (paintedWidth > leftColumnWidth) { leftColumnWidth = paintedWidth } } font: theme.smallestFont text: jobItem["labelName" + index] ? i18nc("placeholder is row description, such as Source or Destination", "%1:", jobItem["labelName" + index]) : "" horizontalAlignment: Text.AlignRight + textFormat: Text.PlainText } PlasmaComponents.Label { id: labelText Layout.fillWidth: true height: paintedHeight font: theme.smallestFont text: jobItem["label" + index] || "" + textFormat: Text.PlainText elide: Text.ElideMiddle PlasmaCore.ToolTipArea { anchors.fill: parent subText: labelText.truncated ? labelText.text : "" + textFormat: Text.PlainText } } } } // The three details rows (eg. how many files and folders have been copied and the total amount etc) Repeater { model: 3 PlasmaComponents.Label { id: detailsLabel anchors { left: parent.left leftMargin: leftColumnWidth + jobItem.layoutSpacing right: parent.right } height: paintedHeight text: localizeProcessedAmount(index) + textFormat: Text.PlainText font: theme.smallestFont visible: text !== "" } } PlasmaComponents.Label { id: speedLabel anchors { left: parent.left leftMargin: leftColumnWidth + jobItem.layoutSpacing right: parent.right } height: paintedHeight font: theme.smallestFont text: eta > 0 ? i18nc("Speed and estimated time to completion", "%1 (%2 remaining)", speed, KCoreAddons.Format.formatSpelloutDuration(eta)) : speed + textFormat: Text.PlainText visible: eta > 0 || parseInt(speed) > 0 } } diff --git a/applets/notifications/package/contents/ui/Jobs.qml b/applets/notifications/package/contents/ui/Jobs.qml index 6a5386e54..0fba93e38 100644 --- a/applets/notifications/package/contents/ui/Jobs.qml +++ b/applets/notifications/package/contents/ui/Jobs.qml @@ -1,141 +1,150 @@ /* * Copyright 2012 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.0 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.plasma.private.notifications 1.0 Column { id: jobsRoot width: parent.width property alias count: jobs.count ListModel { id: jobs } PlasmaCore.DataSource { id: jobsSource property var runningJobs: ({}) engine: "applicationjobs" interval: 0 onSourceAdded: { connectSource(source) jobs.append({name: source}) } onSourceRemoved: { // remove source from jobs model for (var i = 0, len = jobs.count; i < len; ++i) { if (jobs.get(i).name === source) { jobs.remove(i) break } } if (!notifications) { return } var error = runningJobs[source]["error"] var errorText = runningJobs[source]["errorText"] // 1 = ERR_USER_CANCELED - don't show any notification at all if (error == 1) { return } var message = runningJobs[source]["label1"] ? runningJobs[source]["label1"] : runningJobs[source]["label0"] var infoMessage = runningJobs[source]["infoMessage"] if (!message && !infoMessage) { return } var summary = infoMessage ? i18nc("the job, which can be anything, has finished", "%1: Finished", infoMessage) : i18n("Job Finished") if (error) { summary = infoMessage ? i18nc("the job, which can be anything, failed to complete", "%1: Failed", infoMessage) : i18n("Job Failed") } + // notification body interprets HTML, so we need to manually escape the name + var body = (errorText || message || "").replace(/[&<>]/g, function (tag) { + return { + '&': '&', + '<': '<', + '>': '>' + }[tag] || tag + }); + var op = { appIcon: runningJobs[source].appIconName, appName: runningJobs[source].appName, summary: summary, - body: errorText || message, + body: body, isPersistent: !!error, // we'll assume success to be the note-unworthy default, only be persistent in error case urgency: 0, configurable: false, skipGrouping: true, // Bug 360156 actions: !error && UrlHelper.isUrlValid(message) ? ["jobUrl#" + message, i18n("Open...")] : [] }; // If the actionId contains "jobUrl#", it tries to open the "id" value (which is "message") notifications.createNotification(op); delete runningJobs[source] } onNewData: { runningJobs[sourceName] = data } onDataChanged: { var total = 0 for (var i = 0; i < sources.length; ++i) { if (jobsSource.data[sources[i]] && jobsSource.data[sources[i]]["percentage"]) { total += jobsSource.data[sources[i]]["percentage"] } } total /= sources.length notificationsApplet.globalProgress = total/100 } Component.onCompleted: { connectedSources = sources } } Item { visible: jobs.count > 3 PlasmaComponents.ProgressBar { anchors { verticalCenter: parent.verticalCenter left: parent.left right: parent.right } minimumValue: 0 maximumValue: 100 value: notificationsApplet.globalProgress * 100 } } Repeater { model: jobs delegate: JobDelegate {} } }