Commit 2d1b66fb authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Final touches

- Rename some of the classes:
  NotificationServer -> Server (there's namespaces, you know)
  NotificationModel -> NotificationsModel (so it's plural like JobsModel)
- Introduce NotificationGroupingCollapse
- Wire up PulseAudio-qt for eventual notification silence in dnd mode
- Touch up notification looks
  - Add indentation and "line" for grouped plasmoids
  - Rethink "show more" button to be at the end
  - Fix buttons overlapping
  - Remove NotificationDelegate item and do those few adjustments in FullRepresentation
- Cleanup job details, handle when processed > total
- Use States {} more
- Show low urgency popups by default but don't add them to history
parent 9cee05eb
......@@ -35,6 +35,11 @@ set_package_properties(KF5NetworkManagerQt PROPERTIES DESCRIPTION "Qt wrapper fo
PURPOSE "Needed by geolocation data engine."
)
find_package(KF5PulseAudioQt)
set_package_properties(KF5PulseAudioQt PROPERTIES DESCRIPTION "Qt bindings for PulseAudio"
TYPE RECOMMENDED
PURPOSE "Needed so do not disturb mode when disable notification sounds")
find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG)
set_package_properties(KF5Kirigami2 PROPERTIES
DESCRIPTION "A QtQuick based components set"
......
......@@ -18,6 +18,14 @@ target_link_libraries(plasma_applet_notifications
KF5::KIOWidgets # for PreviewJob
)
set(HAVE_PULSEAUDIOQT ${KF5PulseAudioQt_FOUND})
if(KF5PulseAudioQt_FOUND)
target_link_libraries(plasma_applet_notifications KF5::PulseAudioQt)
endif()
# can't ECM do that for us these days?
configure_file(config-notificationsapplet.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-notificationsapplet.h )
install(TARGETS plasma_applet_notifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets)
plasma_install_package(package org.kde.plasma.notifications)
......@@ -21,6 +21,8 @@
#include "notificationapplet.h"
#include "config-notificationsapplet.h"
#include <QClipboard>
#include <QDrag>
#include <QMimeData>
......@@ -32,6 +34,11 @@
#include "filemenu.h"
#include "thumbnailer.h"
#ifdef HAVE_PULSEAUDIOQT
#include <PulseAudioQt/Context>
#include <PulseAudioQt/StreamRestore>
#endif
NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data)
: Plasma::Applet(parent, data)
{
......@@ -43,6 +50,15 @@ NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data
qmlProtectModule(uri, 2);
s_typesRegistered = true;
}
// TODO PulseAudio stuff to mute notification sounds in do not disturb mode
/*PulseAudioQt::Context::instance()->streamRestores();
connect(PulseAudioQt::Context::instance(), &PulseAudioQt::Context::streamRestoreAdded, this, [this](PulseAudioQt::StreamRestore *restore) {
if (restore->name() == QLatin1String("sink-input-by-media-role:event")) {
}
});*/
}
NotificationApplet::~NotificationApplet() = default;
......
/*
* 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 org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
Item {
id: buttonRoot
signal clicked
property alias text: button.text
property alias iconSource: button.iconSource
property alias tooltip: button.tooltip
width: units.iconSizes.small
height: units.iconSizes.small
PlasmaComponents.ToolButton {
id: button
anchors.centerIn: parent
onClicked: buttonRoot.clicked()
}
}
......@@ -107,7 +107,7 @@ GridLayout {
var total = jobDetails["total" + modelData];
if (processed > 0 || total > 1) {
if (processed > 0 && total > 0) {
if (processed > 0 && total > 0 && processed <= total) {
switch(modelData) {
case "Bytes":
return i18nc("How many bytes have been copied", "%2 of %1",
......
/*
* 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 inGroup: notificationItem.inGroup
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
property alias separatorSvg: lineSvgItem.svg
property alias separatorVisible: lineSvgItem.visible
signal configureClicked
signal dismissClicked
signal closeClicked
signal actionInvoked(string actionName)
signal openUrl(string url)
signal fileActionInvoked
signal suspendJobClicked
signal resumeJobClicked
signal killJobClicked
spacing: units.smallSpacing
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)
onFileActionInvoked: delegate.fileActionInvoked()
onSuspendJobClicked: delegate.suspendJobClicked()
onResumeJobClicked: delegate.resumeJobClicked()
onKillJobClicked: delegate.killJobClicked()
}
PlasmaCore.SvgItem {
id: lineSvgItem
elementId: "horizontal-line"
Layout.fillWidth: true
}
}
......@@ -43,13 +43,10 @@ RowLayout {
property string configureActionLabel
property alias expandable: expandButton.visible
property bool expanded
property int collapsedCount
property int expandedCount
property alias configurable: configureButton.visible
property alias dismissable: dismissButton.visible
property bool dismissed
property alias closeButtonTooltip: closeButton.tooltip
property alias closable: closeButton.visible
property var time
......@@ -57,7 +54,6 @@ RowLayout {
property int jobState
property QtObject jobDetails
signal expandClicked
signal configureClicked
signal dismissClicked
signal closeClicked
......@@ -82,7 +78,7 @@ RowLayout {
id: applicationIconItem
Layout.preferredWidth: units.iconSizes.small
Layout.preferredHeight: units.iconSizes.small
source: !notificationHeading.inGroup ? notificationHeading.applicationIconSource : ""
source: notificationHeading.applicationIconSource
usesPlasmaTheme: false
visible: valid
}
......@@ -102,16 +98,7 @@ RowLayout {
// updated periodically by a Timer hence this property with generate() function
property string agoText: ""
visible: text !== ""
text: {
if (expandable) {
if (expanded) {
return expandedCount;
} else {
return i18nc("n more notifications", "+%1", (expandedCount - collapsedCount));
}
}
return generateRemainingText() || agoText;
}
text: generateRemainingText() || agoText
function generateAgoText() {
if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) {
......@@ -185,21 +172,10 @@ RowLayout {
RowLayout {
id: headerButtonsRow
spacing: units.smallSpacing * 2
spacing: 0
Layout.leftMargin: units.smallSpacing
// These aren't ToolButtons so they can be perfectly aligned
// FIXME fix layout overlap
HeaderButton {
id: expandButton
tooltip: notificationHeading.expanded ? i18n("Show Less") : i18n("Show More")
iconSource: notificationHeading.expanded ? "arrow-up" : "arrow-down"
visible: false
onClicked: notificationHeading.expandClicked()
}
HeaderButton {
PlasmaComponents.ToolButton {
id: configureButton
tooltip: notificationHeading.configureActionLabel || i18n("Configure")
iconSource: "configure"
......@@ -207,16 +183,15 @@ RowLayout {
onClicked: notificationHeading.configureClicked()
}
HeaderButton {
PlasmaComponents.ToolButton {
id: dismissButton
tooltip: i18n("Hide")
// FIXME proper icon, perhaps from widgets/configuration-icon
iconSource: "file-zoom-out"
tooltip: notificationHeading.dismissed ? i18nc("Opposite of minimize", "Restore") : i18n("Minimize")
iconSource: notificationHeading.dismissed ? "window-restore" : "window-minimize"
visible: false
onClicked: notificationHeading.dismissClicked()
}
HeaderButton {
PlasmaComponents.ToolButton {
id: closeButton
tooltip: i18n("Close")
iconSource: "window-close"
......@@ -224,4 +199,19 @@ RowLayout {
onClicked: notificationHeading.closeClicked()
}
}
states: [
State {
when: notificationHeading.inGroup
PropertyChanges {
target: applicationIconItem
source: ""
}
PropertyChanges {
target: applicationNameLabel
visible: false
}
}
]
}
......@@ -48,6 +48,7 @@ ColumnLayout {
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
......@@ -69,6 +70,9 @@ ColumnLayout {
property var actionNames: []
property var actionLabels: []
property int headingLeftPadding: 0
property int headingRightPadding: 0
property int thumbnailLeftPadding: 0
property int thumbnailRightPadding: 0
property int thumbnailTopPadding: 0
......@@ -96,9 +100,10 @@ ColumnLayout {
NotificationHeader {
id: notificationHeading
Layout.fillWidth: true
Layout.leftMargin: notificationItem.headingLeftPadding
Layout.rightMargin: notificationItem.headingRightPadding
inGroup: notificationItem.inGroup
parent: inGroup ? inGroupHeaderContainer : defaultHeaderContainer
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
......@@ -116,6 +121,7 @@ ColumnLayout {
// Notification body
RowLayout {
id: bodyRow
Layout.fillWidth: true
spacing: units.smallSpacing
......@@ -124,7 +130,9 @@ ColumnLayout {
spacing: 0
RowLayout {
id: summaryRow
Layout.fillWidth: true
visible: summaryLabel.text !== ""
PlasmaExtras.Heading {
id: summaryLabel
......@@ -141,52 +149,60 @@ ColumnLayout {
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);
if (notificationItem.summary) {
return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary);
} else {
return i18n("Job Failed");
}
} else {
return i18nc("Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary);
if (notificationItem.summary) {
return i18nc("Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary);
} else {
return i18n("Job Finished");
}
}
}
}
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
// 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 !== ""
}
RowLayout {
// When this notification is grouped, the header is reparented here here
id: inGroupHeaderContainer
Layout.fillHeight: true
}
// inGroup headerItem is reparented here
}
SelectableLabel {
id: bodyLabel
Layout.alignment: Qt.AlignVCenter
RowLayout {
id: bodyTextRow
Layout.fillWidth: true
spacing: units.smallSpacing
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)
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 IconItem is reparented here
}
}
PlasmaCore.IconItem {
id: iconItem
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: units.iconSizes.large
Layout.preferredHeight: units.iconSizes.large
usesPlasmaTheme: false
......@@ -282,4 +298,33 @@ ColumnLayout {
onFileActionInvoked: notificationItem.fileActionInvoked()
}
}
states: [
State {
when: notificationItem.inGroup
PropertyChanges {
target: notificationHeading
parent: summaryRow
}
PropertyChanges {
target: summaryRow
visible: true
}
PropertyChanges {
target: summaryLabel
visible: true
}
/*PropertyChanges {
target: bodyLabel.Label
alignment: Qt.AlignTop
}*/
PropertyChanges {
target: iconItem
parent: bodyTextRow
}
}
]
}
......@@ -34,11 +34,9 @@ PlasmaCore.Dialog {
property int popupWidth
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 applicationIconSource: notificationItem.applicationIconSource
property alias deviceName: notificationItem.deviceName
property alias time: notificationItem.time
......@@ -119,7 +117,7 @@ PlasmaCore.Dialog {
mainItem: MouseArea {
id: area
width: notificationPopup.popupWidth
height: notificationItem.implicitHeight
height: notificationItem.implicitHeight + notificationItem.y
hoverEnabled: true
cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor
......@@ -184,6 +182,9 @@ PlasmaCore.Dialog {
NotificationItem {
id: notificationItem
// let the item bleed into the dialog margins so the close button margins cancel out
y: -notificationPopup.margins.top
headingRightPadding: -notificationPopup.margins.right
width: parent.width
hovered: area.containsMouse
maximumLineCount: 8
......
......@@ -42,11 +42,6 @@ QtObject {
property bool inhibited: false
// Reset the expire limiter so we don't get a flood of non-expired notifications
onInhibitedChanged: {
}
// Some parts of the code rely on plasmoid.nativeInterface and since we're in a singleton here
// this is named "plasmoid"
property QtObject plasmoid: plasmoids[0]
......@@ -112,7 +107,7 @@ QtObject {
// How much vertical screen real estate the notification popups may consume
readonly property real popupMaximumScreenFill: 0.75
property var screenRect: plasmoid.availableScreenRect
property var screenRect: plasmoid ? plasmoid.availableScreenRect : undefined
onPopupLocationChanged: Qt.callLater(positionPopups)
onScreenRectChanged: Qt.callLater(positionPopups)
......@@ -244,7 +239,7 @@ QtObject {
}
property QtObject popupNotificationsModel: NotificationManager.Notifications {
limit: Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))
limit: globals.screenRect ? (Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0
showExpired: false
showDismissed: false
blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
......@@ -303,7 +298,7 @@ QtObject {
notificationType: model.type
applicationName: model.applicationName
applicatonIconSource: model.applicationIconName
applicationIconSource: model.applicationIconName
deviceName: model.deviceName || ""
time: model.updated || model.created
......@@ -330,7 +325,7 @@ QtObject {
? defaultTimeout : 0
urls: model.urls || []
urgency: model.urgency
urgency: model.urgency || NotificationManager.Notifications.NormalUrgency
jobState: model.jobState || 0
percentage: model.percentage || 0
......
......@@ -34,7 +34,12 @@ import "global"
Item {
id: root
Plasmoid.status: PlasmaCore.Types.PassiveStatus
Plasmoid.hideOnWindowDeactivate: false
Plasmoid.status: historyModel.activeJobsCount > 0
|| Globals.popupNotificationsModel.activeNotificationsCount > 0
|| Globals.inhibited ? PlasmaCore.Types.ActiveStatus
: PlasmaCore.Types.PassiveStatus
Plasmoid.toolTipSubText: {
var lines = [];
......@@ -87,29 +92,6 @@ Item {
}
// Delay hiding the applet again so the user can see the unread count briefly before it goes away
Timer {
id: updateStatusTimer
readonly property int targetStatus: historyModel.activeJobsCount > 0
|| Globals.popupNotificationsModel.activeNotificationsCount > 0