Commit e93bba28 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Replace old notification plasmoid by new one

Our dear friend Notification plasmoid has slipped the surly bonds of Earth
to touch the face of eternal preservation in Git history. It leaves a huge
emptiness in the hearts of all of us who loved it, many who here its friends.
Originally concveived in 2011 as a "New QML based notifications plasmoid
for mobile", it quickly outgrew its creator's vision and became a vital
component of the Plasma Desktop. When the new kid on the block arrived in
the form of Plasma 5, it showed its willpower to survive this stark transition
and served us for another half a decade. It taught us to seize every
opportunity to increase productivity and improve user experience,
yet it became evident that it reached its limits and so on April Fools' Day 2019
its heart beat for the last time.

Farewell!
parent 16d06c3b
......@@ -18,6 +18,5 @@ if(NOT WIN32)
# #notifications
# #should compile also on windows? (even if doesn't really make sense)
add_subdirectory(notifications)
add_subdirectory(newnotifications)
add_subdirectory(systemtray)
endif()
add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.notifications\")
set(notificationapplet_SRCS
notificationapplet.cpp
filemenu.cpp
thumbnailer.cpp
)
add_library(plasma_applet_newnotifications MODULE ${notificationapplet_SRCS}) # FIXME
kcoreaddons_desktop_to_json(plasma_applet_newnotifications package/metadata.desktop) # FIXME
target_link_libraries(plasma_applet_newnotifications # FIXME
# shouldnt require it as it's all QML, isn't it?
#PW::NotificationManager
Qt5::Gui
Qt5::Quick # for QQmlParserStatus
KF5::I18n
KF5::Plasma
KF5::KIOWidgets # for PreviewJob
)
install(TARGETS plasma_applet_newnotifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets) # FIXME
plasma_install_package(package org.kde.plasma.newnotifications)
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="General">
</group>
</kcfg>
/*
* 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 org.kde.plasma.core 2.0 as PlasmaCore
ColumnLayout {
id: delegate
property alias notificationType: notificationItem.notificationType
property alias headerVisible: notificationItem.headerVisible
property alias applicationName: notificationItem.applicationName
property alias applicatonIconSource: notificationItem.applicationIconSource
property alias deviceName: notificationItem.deviceName
property alias time: notificationItem.time
property alias summary: notificationItem.summary
property alias body: notificationItem.body
property alias icon: notificationItem.icon
property alias urls: notificationItem.urls
property alias jobState: notificationItem.jobState
property alias percentage: notificationItem.percentage
property alias error: notificationItem.error
property alias errorText: notificationItem.errorText
property alias suspendable: notificationItem.suspendable
property alias killable: notificationItem.killable
property alias jobDetails: notificationItem.jobDetails
property alias configureActionLabel: notificationItem.configureActionLabel
property alias configurable: notificationItem.configurable
property alias dismissable: notificationItem.dismissable
property alias closable: notificationItem.closable
property alias actionNames: notificationItem.actionNames
property alias actionLabels: notificationItem.actionLabels
signal configureClicked
signal dismissClicked
signal closeClicked
//signal defaultActionInvoked
signal actionInvoked(string actionName)
signal openUrl(string url)
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
// FIXME
property alias svg: lineSvgItem.svg
spacing: 0
NotificationItem {
id: notificationItem
Layout.fillWidth: true
closable: true
onCloseClicked: delegate.closeClicked()
onDismissClicked: delegate.dismissClicked()
onConfigureClicked: delegate.configureClicked()
onActionInvoked: delegate.actionInvoked(actionName)
onOpenUrl: delegate.openUrl(url)
onSuspendJobClicked: delegate.suspendJobClicked()
onResumeJobClicked: delegate.resumeJobClicked()
onKillJobClicked: delegate.killJobClicked()
}
PlasmaCore.SvgItem {
id: lineSvgItem
elementId: "horizontal-line"
Layout.fillWidth: true
// TODO hide for last notification
}
}
/*
* Copyright 2018-2019 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
ColumnLayout {
id: notificationItem
property bool hovered: false
property int maximumLineCount: 0
property alias bodyCursorShape: bodyLabel.cursorShape
property int notificationType
property bool headerVisible: true
property alias applicationIconSource: notificationHeading.applicationIconSource
property alias applicationName: notificationHeading.applicationName
property alias deviceName: notificationHeading.deviceName
property string summary
property alias time: notificationHeading.time
property alias configurable: notificationHeading.configurable
property alias dismissable: notificationHeading.dismissable
property alias closable: notificationHeading.closable
// This isn't an alias because TextEdit RichText adds some HTML tags to it
property string body
property alias icon: iconItem.source
property var urls: []
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 alias configureActionLabel: notificationHeading.configureActionLabel
property var actionNames: []
property var actionLabels: []
property int thumbnailLeftPadding: 0
property int thumbnailRightPadding: 0
property int thumbnailTopPadding: 0
property int thumbnailBottomPadding: 0
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
signal bodyClicked(var mouse)
signal closeClicked
signal configureClicked
signal dismissClicked
signal actionInvoked(string actionName)
signal openUrl(string url)
signal fileActionInvoked
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
spacing: units.smallSpacing
NotificationHeader {
id: notificationHeading
Layout.fillWidth: true
visible: notificationItem.headerVisible
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
jobDetails: notificationItem.jobDetails
onConfigureClicked: notificationItem.configureClicked()
onDismissClicked: notificationItem.dismissClicked()
onCloseClicked: 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
maximumLineCount: 3
wrapMode: Text.WordWrap
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() !== notificationItem.applicationName.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 != notificationItem.applicationIconSource
}
}
// Job progress reporting
Loader {
id: jobLoader
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()
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked()
hovered: notificationItem.hovered
}
}
RowLayout {
Layout.fillWidth: true
// 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)
}
}
}
}
// thumbnails
Loader {
id: thumbnailStripLoader
Layout.leftMargin: notificationItem.thumbnailLeftPadding
Layout.rightMargin: notificationItem.thumbnailRightPadding
Layout.topMargin: notificationItem.thumbnailTopPadding
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: -thumbnailStripLoader.Layout.topMargin
bottomPadding: -thumbnailStripLoader.Layout.bottomMargin
urls: notificationItem.urls
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked()
}
}
}
/*
* Copyright 2019 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 org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as Components
import org.kde.notificationmanager 1.0 as NotificationManager
import ".."
PlasmaCore.Dialog {
id: notificationPopup
property alias notificationType: notificationItem.notificationType
//readonly property bool isNotification: notificationType === NotificationManager.Notifications.NotificationType
//readonly property bool isJob: notificationType === NotificationManager.Notifications.JobType
property alias applicationName: notificationItem.applicationName
property alias applicatonIconSource: notificationItem.applicationIconSource
property alias deviceName: notificationItem.deviceName
property alias time: notificationItem.time
property alias summary: notificationItem.summary
property alias body: notificationItem.body
property alias icon: notificationItem.icon
property alias urls: notificationItem.urls
property int urgency
property int timeout
property int dismissTimeout
property alias jobState: notificationItem.jobState
property alias percentage: notificationItem.percentage
property alias error: notificationItem.error
property alias errorText: notificationItem.errorText
property alias suspendable: notificationItem.suspendable
property alias killable: notificationItem.killable
property alias jobDetails: notificationItem.jobDetails
property alias configureActionLabel: notificationItem.configureActionLabel
property alias configurable: notificationItem.configurable
property alias dismissable: notificationItem.dismissable
property alias closable: notificationItem.closable
property bool hasDefaultAction
property alias actionNames: notificationItem.actionNames
property alias actionLabels: notificationItem.actionLabels
signal configureClicked
signal dismissClicked
signal closeClicked
signal defaultActionInvoked
signal actionInvoked(string actionName)
signal openUrl(string url)
signal fileActionInvoked
signal expired
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
property int defaultTimeout: 5000
readonly property int effectiveTimeout: {
if (timeout === -1) {
return defaultTimeout;
}
if (dismissTimeout) {
return dismissTimeout;
}
return timeout;
}
location: PlasmaCore.Types.Floating
type: PlasmaCore.Dialog.Notification
flags: {
var flags = Qt.WindowDoesNotAcceptFocus;
if (urgency === NotificationManager.Notifications.CriticalUrgency) {
flags |= Qt.WindowStaysOnTopHint;
}
return flags;
}
visible: false
// When notification is updated, restart hide timer
onTimeChanged: {
if (timer.running) {
timer.restart();
}
}
mainItem: MouseArea {
id: area
width: popupHandler.popupWidth
height: notificationItem.implicitHeight
hoverEnabled: true
cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: hasDefaultAction ? Qt.LeftButton : Qt.NoButton
onClicked: notificationPopup.defaultActionInvoked()
Timer {
id: timer
interval: notificationPopup.effectiveTimeout
running: notificationPopup.visible && !area.containsMouse && interval > 0
&& !notificationItem.dragging && !notificationItem.menuOpen
onTriggered: {
if (notificationPopup.dismissTimeout) {
notificationPopup.dismissClicked();
} else {
notificationPopup.expired();
}
}
}
Timer {
id: timeoutIndicatorDelayTimer
// only show indicator for the last ten seconds of timeout
readonly property int remainingTimeout: 10000
interval: Math.max(0, timer.interval - remainingTimeout)
running: interval > 0 && timer.running
}
Rectangle {
id: timeoutIndicatorRect
anchors {
right: parent.right
rightMargin: -notificationPopup.margins.right
bottom: parent.bottom
bottomMargin: -notificationPopup.margins.bottom
}
width: units.devicePixelRatio * 3
radius: width
color: theme.highlightColor
opacity: timeoutIndicatorAnimation.running ? 0.6 : 0
visible: units.longDuration > 1
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
}
}
NumberAnimation {
id: timeoutIndicatorAnimation
target: timeoutIndicatorRect
property: "height"
from: area.height + notificationPopup.margins.top + notificationPopup.margins.bottom
to: 0
duration: Math.min(timer.interval, timeoutIndicatorDelayTimer.remainingTimeout)
running: timer.running && !timeoutIndicatorDelayTimer.running && units.longDuration > 1
}
}
NotificationItem {
id: notificationItem
width: parent.width
hovered: area.containsMouse
maximumLineCount: 8 // TODO configurable?
bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0