diff --git a/applets/notifications/package/contents/config/main.xml b/applets/notifications/package/contents/config/main.xml index 6e55fe88b..362819735 100644 --- a/applets/notifications/package/contents/config/main.xml +++ b/applets/notifications/package/contents/config/main.xml @@ -1,19 +1,23 @@ true true + + + true + diff --git a/applets/notifications/package/contents/ui/NotificationDelegate.qml b/applets/notifications/package/contents/ui/NotificationDelegate.qml index 8dd567f95..e445a8b12 100644 --- a/applets/notifications/package/contents/ui/NotificationDelegate.qml +++ b/applets/notifications/package/contents/ui/NotificationDelegate.qml @@ -1,165 +1,167 @@ /* * Copyright 2011 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 QtQuick.Controls.Private 1.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.kquickcontrolsaddons 2.0 PlasmaComponents.ListItem { id: notificationItem width: popupFlickable.width + + property ListModel listModel; opacity: 1-Math.abs(x)/width enabled: model.hasDefaultAction checked: notificationItem.containsMouse Timer { interval: 10*60*1000 repeat: false running: !idleTimeSource.idle onTriggered: { - if (!notificationsModel.inserting) - notificationsModel.remove(index) + if (!listModel.inserting) + listModel.remove(index) } } MouseArea { width: parent.width height: childrenRect.height acceptedButtons: Qt.NoButtons drag { target: notificationItem axis: Drag.XAxis //kind of an hack over Column being too smart minimumX: -parent.width + 1 maximumX: parent.width - 1 } onReleased: { if (notificationItem.x < -notificationItem.width/2) { removeAnimation.exitFromRight = false removeAnimation.running = true } else if (notificationItem.x > notificationItem.width/2 ) { removeAnimation.exitFromRight = true removeAnimation.running = true } else { resetAnimation.running = true } } SequentialAnimation { id: removeAnimation property bool exitFromRight: true NumberAnimation { target: notificationItem properties: "x" to: removeAnimation.exitFromRight ? notificationItem.width-1 : 1-notificationItem.width duration: units.longDuration easing.type: Easing.InOutQuad } NumberAnimation { target: notificationItem properties: "height" to: 0 duration: units.longDuration easing.type: Easing.InOutQuad } ScriptAction { script: { closeNotification(model.source); - notificationsModel.remove(index); + listModel.remove(index); } } } SequentialAnimation { id: resetAnimation NumberAnimation { target: notificationItem properties: "x" to: 0 duration: units.longDuration easing.type: Easing.InOutQuad } } NotificationItem { id: notification width: parent.width compact: true icon: appIcon image: model.image summary: model.summary body: model.body configurable: model.configurable && !Settings.isMobile // model.actions JS array is implicitly turned into a ListModel which we can assign directly actions: model.actions created: model.created hasDefaultAction: model.hasDefaultAction hasConfigureAction: model.hasConfigureAction urls: { // QML ListModel tries to be smart and turns our urls Array into a dict with index as key... var urls = [] var modelUrls = model.urls if (modelUrls) { for (var key in modelUrls) { urls.push(modelUrls[key]) } } return urls } onClose: { - if (notificationsModel.count > 1) { + if (listModel.count > 1) { removeAnimation.running = true } else { closeNotification(model.source) - notificationsModel.remove(index) + listModel.remove(index) } } onConfigure: { plasmoid.expanded = false configureNotification(model.appRealName, model.eventId) } onAction: { executeAction(model.source, actionId) actions.clear() } onOpenUrl: { plasmoid.expanded = false Qt.openUrlExternally(url) } } } //MouseArea Component.onCompleted: { mainScrollArea.height = mainScrollArea.implicitHeight } Component.onDestruction: { mainScrollArea.height = mainScrollArea.implicitHeight } } diff --git a/applets/notifications/package/contents/ui/NotificationIcon.qml b/applets/notifications/package/contents/ui/NotificationIcon.qml index 0c27cfd1f..669fce63f 100644 --- a/applets/notifications/package/contents/ui/NotificationIcon.qml +++ b/applets/notifications/package/contents/ui/NotificationIcon.qml @@ -1,165 +1,165 @@ /*************************************************************************** * Copyright 2011 Davide Bettio * * Copyright 2011 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 of the License, 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.kquickcontrolsaddons 2.0 Item { PlasmaCore.SvgItem { id: notificationSvgItem anchors.centerIn: parent width: units.roundToIconSize(Math.min(parent.width, parent.height)) height: width svg: notificationSvg elementId: { - if (totalCount > 0) { + if (activeItemsCount > 0) { if (jobs && jobs.count > 0) { return "notification-progress-inactive" } else { return "notification-empty" } } return "notification-disabled" } state: notificationsApplet.state PlasmaCore.Svg { id: notificationSvg imagePath: "icons/notification" colorGroup: PlasmaCore.ColorScope.colorGroup } Item { id: jobProgressItem anchors { left: parent.left top: parent.top bottom: parent.bottom } width: notificationSvgItem.width * globalProgress clip: true visible: jobs.count > 0 PlasmaCore.SvgItem { anchors { left: parent.left top: parent.top bottom: parent.bottom } width: notificationSvgItem.width svg: notificationSvg elementId: "notification-progress-active" } } PlasmaComponents.BusyIndicator { anchors.fill: parent visible: jobs ? jobs.count > 0 : false running: visible } PlasmaComponents.Label { id: notificationCountLabel - property int oldTotalCount: 0 + property int oldActiveItemsCount: 0 // anchors.fill: parent breaks at small sizes for some reason anchors.centerIn: parent width: parent.width - (units.smallSpacing * 2.5 * text.length) height: width horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: notificationsApplet.totalCount + text: notificationsApplet.activeItemsCount font.pointSize: 100 fontSizeMode: Text.Fit minimumPointSize: theme.smallestFont.pointSize - visible: notificationsApplet.totalCount > 0 + visible: notificationsApplet.activeItemsCount > 0 Connections { target: notificationsApplet - onTotalCountChanged: { - if (notificationsApplet.totalCount > notificationCountLabel.oldTotalCount) { + onActiveItemsCountChanged: { + if (notificationsApplet.activeItemsCount > notificationCountLabel.oldActiveItemsCount) { notificationAnimation.running = true } - notificationCountLabel.oldTotalCount = notificationsApplet.totalCount + notificationCountLabel.oldActiveItemsCount = notificationsApplet.activeItemsCount } } } PlasmaCore.SvgItem { id: notificationAnimatedItem anchors.fill: parent svg: notificationSvg elementId: "notification-active" opacity: 0 scale: 2 SequentialAnimation { id: notificationAnimation NumberAnimation { target: notificationAnimatedItem duration: units.longDuration properties: "opacity, scale" to: 1 easing.type: Easing.InOutQuad } PauseAnimation { duration: units.longDuration * 2 } ParallelAnimation { NumberAnimation { target: notificationAnimatedItem duration: units.longDuration properties: "opacity" to: 0 easing.type: Easing.InOutQuad } NumberAnimation { target: notificationAnimatedItem duration: units.longDuration properties: "scale" to: 2 easing.type: Easing.InOutQuad } } } } MouseArea { anchors.fill: parent property bool wasExpanded: false onPressed: wasExpanded = plasmoid.expanded onClicked: plasmoid.expanded = !wasExpanded } } } diff --git a/applets/notifications/package/contents/ui/Notifications.qml b/applets/notifications/package/contents/ui/Notifications.qml index 3680eb829..1f9309806 100644 --- a/applets/notifications/package/contents/ui/Notifications.qml +++ b/applets/notifications/package/contents/ui/Notifications.qml @@ -1,225 +1,281 @@ /* * 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: notificationsRoot anchors { left: parent.left right: parent.right } property alias count: notificationsRepeater.count + property alias historyCount: notificationsHistoryRepeater.count + + property bool showHistory + signal popupShown(var popup) + + onShowHistoryChanged: { + if(!showHistory) + clearHistory() + } Component.onCompleted: { // Create the popup components and pass them to the C++ plugin for (var i = 0; i < 3; i++) { var popup = notificationPopupComponent.createObject(); notificationPositioner.addNotificationPopup(popup); } } function addNotification(notification) { // Do not show duplicated notifications for (var i = 0; i < notificationsModel.count; ++i) { if (notificationsModel.get(i).source == notification.source && notificationsModel.get(i).appName == notification.appName && notificationsModel.get(i).summary == notification.summary && notificationsModel.get(i).body == notification.body) { return } } for (var i = 0; i < notificationsModel.count; ++i) { if (notificationsModel.get(i).source == notification.source || (notificationsModel.get(i).appName == notification.appName && notificationsModel.get(i).summary == notification.summary && notificationsModel.get(i).body == notification.body)) { notificationsModel.remove(i) break } } if (notificationsModel.count > 20) { notificationsModel.remove(notificationsModel.count-1) } if (notification.isPersistent) { notification.created = new Date(); notificationsModel.inserting = true; notificationsModel.insert(0, notification); notificationsModel.inserting = false; } + else if (showHistory) { + + notificationsHistoryModel.inserting = true; + + //create a copy of the notification. + //Disable actions in this copy as they will stop working once the original notification is closed. + notificationsHistoryModel.insert(0, { + "compact" : notification.compact, + "icon" : notification.icon, + "image" : notification.image, + "summary" : notification.summary, + "body" : notification.body, + "configurable" : false, + "created" : new Date(), + "urls" : notification.urls, + "maximumTextHeight" : notification.maximumTextHeight, + "actions" : null, + "hasDefaultAction" : false, + "hasConfigureAction" : false, + }); + notificationsHistoryModel.inserting = false; + } notificationPositioner.displayNotification(notification); } function executeAction(source, id) { //try to use the service if (id.indexOf("jobUrl#") === -1) { var service = notificationsSource.serviceForSource(source) var op = service.operationDescription("invokeAction") op["actionId"] = id service.startOperationCall(op) //try to open the id as url } else if (id.indexOf("jobUrl#") !== -1) { Qt.openUrlExternally(id.slice(7)); } notificationPositioner.closePopup(source); } function configureNotification(appRealName, eventId) { var service = notificationsSource.serviceForSource("notification") var op = service.operationDescription("configureNotification") op.appRealName = appRealName op.eventId = eventId service.startOperationCall(op) } function createNotification(data) { var service = notificationsSource.serviceForSource("notification"); var op = service.operationDescription("createNotification"); // add everything from "data" to "op" for (var attrname in data) { op[attrname] = data[attrname]; } service.startOperationCall(op); } function closeNotification(source) { var service = notificationsSource.serviceForSource(source) var op = service.operationDescription("userClosed") service.startOperationCall(op) } function expireNotification(source) { var service = notificationsSource.serviceForSource(source) var op = service.operationDescription("expireNotification") service.startOperationCall(op) } function clearNotifications() { for (var i = 0, length = notificationsSource.sources.length; i < length; ++i) { var source = notificationsSource.sources[i]; closeNotification(source) notificationPositioner.closePopup(source); } notificationsModel.clear() + clearHistory() + } + + function clearHistory() { + notificationsHistoryModel.clear() } Component { id: notificationPopupComponent NotificationPopup { } } ListModel { id: notificationsModel property bool inserting: false } + + ListModel { + id: notificationsHistoryModel + property bool inserting: false + } PlasmaCore.DataSource { id: idleTimeSource property bool idle: data["UserActivity"]["IdleTime"] > 300000 engine: "powermanagement" interval: 30000 connectedSources: ["UserActivity"] //Idle with more than 5 minutes of user inactivity } PlasmaCore.DataSource { id: notificationsSource engine: "notifications" interval: 0 onSourceAdded: { connectSource(source); } onSourceRemoved: { notificationPositioner.closePopup(source); for (var i = 0; i < notificationsModel.count; ++i) { if (notificationsModel.get(i).source == source) { notificationsModel.remove(i) break } } } onNewData: { var _data = data; // Temp copy to avoid lots of context switching var actions = [] _data["hasDefaultAction"] = false if (data["actions"] && data["actions"].length % 2 == 0) { for (var i = 0; i < data["actions"].length; i += 2) { var action = data["actions"][i] if (action == "default") { // The default action is not shown, but we want to know it's there _data["hasDefaultAction"] = true } else if (action == "settings") { // configure icon in the notification for custom notification settings _data["hasConfigureAction"] = true; _data["configurable"] = true; } else { actions.push({ id: data["actions"][i], text: data["actions"][i+1] }) } } } _data["source"] = sourceName _data["actions"] = actions notificationsRoot.addNotification(_data) } } Connections { target: plasmoid.nativeInterface onAvailableScreenRectChanged: { notificationPositioner.setPlasmoidScreenGeometry(availableScreenRect); } } NotificationsHelper { id: notificationPositioner popupLocation: plasmoid.nativeInterface.screenPosition Component.onCompleted: { notificationPositioner.setPlasmoidScreenGeometry(plasmoid.nativeInterface.availableScreenRect); } onPopupShown: notificationsRoot.popupShown(popup) } Repeater { id: notificationsRepeater model: notificationsModel - delegate: NotificationDelegate {} + delegate: NotificationDelegate { listModel: notificationsModel } + } + + PlasmaExtras.Heading { + width: parent.width + level: 3 + opacity: 0.6 + visible: historyCount > 0 + text: i18n("History") + } + + Repeater { + id: notificationsHistoryRepeater + model: notificationsHistoryModel + delegate: NotificationDelegate { listModel: notificationsHistoryModel } } } diff --git a/applets/notifications/package/contents/ui/configNotifications.qml b/applets/notifications/package/contents/ui/configNotifications.qml index 65406d0cb..c330a8ccf 100644 --- a/applets/notifications/package/contents/ui/configNotifications.qml +++ b/applets/notifications/package/contents/ui/configNotifications.qml @@ -1,67 +1,74 @@ /* * Copyright 2014 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.0 import QtQuick.Controls 1.0 as QtControls import QtQuick.Layouts 1.1 as QtLayouts import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.private.notifications 1.0 Item { id: appearancePage width: childrenRect.width height: childrenRect.height signal configurationChanged property alias cfg_showNotifications: showNotificationsCheckBox.checked property alias cfg_showJobs: showJobsCheckBox.checked + property alias cfg_showHistory: showHistoryCheckBox.checked QtLayouts.ColumnLayout { anchors.left: parent.left QtControls.CheckBox { id: showNotificationsCheckBox text: i18n("Show application and system notifications") } QtControls.CheckBox { id: showJobsCheckBox text: i18n("Track file transfers and other jobs") } + + QtControls.CheckBox { + id: showHistoryCheckBox + text: i18n("Show a history of notifications") + } + QtControls.CheckBox { id: useCustomPopupPositionCheckBox text: i18n("Use custom position for the notification popup") checked: plasmoid.nativeInterface.configScreenPosition() != NotificationsHelper.Default } ScreenPositionSelector { id: screenPositionSelector enabled: useCustomPopupPositionCheckBox.checked selectedPosition: plasmoid.nativeInterface.screenPosition disabledPositions: [NotificationsHelper.Left, NotificationsHelper.Center, NotificationsHelper.Right] } } Component.onCompleted: { plasmoid.nativeInterface.screenPosition = Qt.binding(function() {configurationChanged(); return screenPositionSelector.selectedPosition; }); } } diff --git a/applets/notifications/package/contents/ui/main.qml b/applets/notifications/package/contents/ui/main.qml index 4d1a81653..9802ced46 100644 --- a/applets/notifications/package/contents/ui/main.qml +++ b/applets/notifications/package/contents/ui/main.qml @@ -1,180 +1,184 @@ /*************************************************************************** * Copyright 2011 Davide Bettio * * Copyright 2011 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 of the License, 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.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.private.notifications 1.0 import "uiproperties.js" as UiProperties MouseEventListener { id: notificationsApplet //width: units.gridUnit.width * 10 //height: units.gridUnit.width * 15 //Layout.minimumWidth: mainScrollArea.implicitWidth //Layout.minimumHeight: mainScrollArea.implicitHeight Layout.minimumWidth: 256 // FIXME: use above Layout.minimumHeight: 256 Layout.maximumWidth: -1 Layout.maximumHeight: mainScrollArea.implicitHeight LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true property int layoutSpacing: UiProperties.layoutSpacing property real globalProgress: 0 property Item notifications: notificationsLoader.item property Item jobs: jobsLoader.item - + //notifications + jobs - property int totalCount: (notifications ? notifications.count : 0) + (jobs ? jobs.count : 0) + property int activeItemsCount: (notifications ? notifications.count : 0) + (jobs ? jobs.count : 0) + property int totalCount: activeItemsCount + (notifications ? notifications.historyCount : 0) Plasmoid.switchWidth: units.gridUnit * 20 Plasmoid.switchHeight: units.gridUnit * 30 - Plasmoid.status: totalCount > 0 ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus + Plasmoid.status: activeItemsCount > 0 ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus Plasmoid.icon: { if (jobs && jobs.count) { return "notification-active" } - return totalCount ? "notification-inactive" : "notification-disabled" + return activeItemsCount ? "notification-inactive" : "notification-disabled" } Plasmoid.toolTipSubText: { - if (totalCount == 0) { + if (activeItemsCount == 0) { return i18n("No notifications or jobs") } else if (!notifications || !notifications.count) { return i18np("%1 running job", "%1 running jobs", jobs.count) } else if (!jobs || !jobs.count) { return i18np("%1 notification", "%1 notifications", notifications.count) } else { return i18np("%1 running job", "%1 running jobs", jobs.count) + "\n" + i18np("%1 notification", "%1 notifications", notifications.count) } } Plasmoid.compactRepresentation: NotificationIcon { } hoverEnabled: !UiProperties.touchInput - onTotalCountChanged: { - if (!totalCount) { + onActiveItemsCountChanged: { + if (!activeItemsCount) { plasmoid.expanded = false; } } PlasmaCore.Svg { id: configIconsSvg imagePath: "widgets/configuration-icons" } PlasmaExtras.Heading { width: parent.width level: 3 opacity: 0.6 visible: notificationsApplet.totalCount == 0 text: i18n("No new notifications.") } PlasmaExtras.ScrollArea { id: mainScrollArea anchors.fill: parent implicitWidth: theme.mSize(theme.defaultFont).width * 40 implicitHeight: Math.min(theme.mSize(theme.defaultFont).height * 40, Math.max(theme.mSize(theme.defaultFont).height * 6, contentsColumn.height)) state: "" Flickable { id: popupFlickable anchors.fill:parent contentWidth: width contentHeight: contentsColumn.height clip: true Column { id: contentsColumn width: popupFlickable.width Loader { id: jobsLoader width: parent.width source: "Jobs.qml" active: notificationsApplet.Plasmoid.configuration.showJobs } Loader { id: notificationsLoader width: parent.width source: "Notifications.qml" active: notificationsApplet.Plasmoid.configuration.showNotifications + onLoaded: { + notificationsLoader.item.showHistory = Qt.binding(function(){ return notificationsApplet.Plasmoid.configuration.showHistory }) + } } } } states: [ State { name: "underMouse" when: notificationsApplet.containsMouse PropertyChanges { target: mainScrollArea implicitHeight: implicitHeight } }, State { name: "" when: !notificationsApplet.containsMouse PropertyChanges { target: mainScrollArea implicitHeight: Math.min(theme.mSize(theme.defaultFont).height * 40, Math.max(theme.mSize(theme.defaultFont).height * 6, contentsColumn.height)) } } ] } function action_clearNotifications() { notifications.clearNotifications() } function action_notificationskcm() { KCMShell.open("kcmnotify"); } Component.onCompleted: { plasmoid.setAction("clearNotifications", i18n("Clear Notifications"), "edit-clear") var clearAction = plasmoid.action("clearNotifications"); clearAction.visible = Qt.binding(function() { return notificationsApplet.notifications && notificationsApplet.notifications.count > 0 }) if (KCMShell.authorize("kcmnotify.desktop").length > 0) { plasmoid.setAction("notificationskcm", i18n("&Configure Event Notifications and Actions..."), "preferences-desktop-notification") } } }