Commit 79427e3d authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Cleanup, export, and start on settings and inhibition API

- Cleanup, pimpl NotificationServer and Notification for export
- Wire the old dataengine up to use it for compat:
  it shows notifications but is otherwise pretty broken right now
- Start writing kconfigxt and Settings code (no real code yet)
- Add a Inhibition DBus interface (no actual server-side code yet)
- Group notifications by application
  Pretty much tasks grouping model from libtaskmanager slightly adjusted
- Let widget take full height when expanded in vertical panel "side bar usecase"
- Fix bugs here and there
parent 3251c280
......@@ -31,6 +31,7 @@ import org.kde.notificationmanager 1.0 as NotificationManager
ColumnLayout {
Layout.preferredWidth: units.gridUnit * 22
Layout.preferredHeight: units.gridUnit * 28
Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical
RowLayout {
Layout.fillWidth: true
......@@ -122,26 +123,46 @@ ColumnLayout {
}
}*/
delegate: NotificationDelegate {
width: list.width
delegate: Loader {
sourceComponent: model.isGroup ? groupDelegate : notificationDelegate
applicationName: model.applicationName
applicatonIconSource: model.applicationIconName
Component {
id: groupDelegate
NotificationHeader {
width: list.width
time: model.updated || model.created
applicationName: model.applicationName
applicationIconSource: model.applicationIconName
configurable: model.configurable
time: model.updated || model.created
}
summary: model.summary
body: model.body || "" // TODO
icon: model.image || model.iconName
}
Component {
id: notificationDelegate
NotificationDelegate {
width: list.width
applicationName: model.applicationName
applicatonIconSource: model.applicationIconName
time: model.updated || model.created
// TODO everything else
configurable: model.configurable
onCloseClicked: historyModel.close(historyModel.index(index, 0))
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
summary: model.summary
body: model.body || "" // TODO
icon: model.image || model.iconName
svg: lineSvg
// TODO everything else
onCloseClicked: historyModel.close(historyModel.index(index, 0))
onConfigureClicked: historyModel.configure(historyModel.index(index, 0))
svg: lineSvg
}
}
}
}
}
......
/*
* 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
import org.kde.kcoreaddons 1.0 as KCoreAddons
RowLayout {
id: notificationHeading
property int notificationType
property alias applicationIconSource: applicationIconItem.source
property string applicationName
property string configureActionLabel
property alias configurable: configureButton.visible
property alias dismissable: dismissButton.visible
property alias closable: closeButton.visible
property var time
property int jobState
property QtObject jobDetails
signal configureClicked
signal dismissClicked
signal closeClicked
function updateAgoText() {
if (time && !isNaN(time.getTime())) {
var now = new Date();
var deltaMinutes = Math.floor((now.getTime() - time.getTime()) / 1000 / 60);
if (deltaMinutes > 0) {
ageLabel.agoText = i18ncp("Received minutes ago, keep short", "%1 min ago", "%1 min ago", deltaMinutes);
return;
}
}
ageLabel.agoText = "";
}
spacing: units.smallSpacing
Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, units.iconSizes.small)
// TODO this timer should probably be at a central location
// so every notification updates simultaneously
Timer {
interval: 60000
repeat: true
running: notificationHeading.visible
&& notificationHeading.Window.window
&& notificationHeading.Window.window.visible
triggeredOnStart: true
onTriggered: notificationHeading.updateAgoText()
}
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
text: notificationHeading.applicationName// + (notificationHeading.deviceName ? " · " + notificationHeading.deviceName : "")
}
PlasmaExtras.DescriptiveLabel {
id: ageLabel
// the "n minutes ago" text, for jobs we show remaining time instead
property string agoText: ""
visible: text !== ""
text: remainingText() || agoText
function remainingText() {
if (notificationHeading.notificationType !== NotificationManager.Notifications.JobType
|| notificationHeading.jobState === NotificationManager.Notifications.JobStateStopped) {
return;
}
var details = notificationHeading.jobDetails;
if (!details || !details.speed) {
return;
}
var remaining = details.totalBytes - details.processedBytes;
if (remaining <= 0) {
return;
}
var eta = remaining / details.speed;
if (!eta) {
return;
}
if (eta < 60 * 60) { // 1 hr
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));
}
}
function updateAgoText() {
}
}
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: notificationHeading.configureActionLabel || i18n("Configure")
iconSource: "configure"
visible: false
onClicked: notificationHeading.configureClicked()
}
HeaderButton {
id: dismissButton
tooltip: i18n("Hide")
// FIXME proper icon, perhaps from widgets/configuration-icon
iconSource: "file-zoom-out"
visible: false
onClicked: notificationHeading.dismissClicked()
}
HeaderButton {
id: closeButton
tooltip: i18n("Close")
iconSource: "window-close"
visible: false
onClicked: notificationHeading.closeClicked()
}
}
}
}
......@@ -39,20 +39,21 @@ ColumnLayout {
property int notificationType
property alias applicationIconSource: applicationIconItem.source
property alias applicationName: applicationNameLabel.text
property alias applicationIconSource: notificationHeading.applicationIconSource
property alias applicationName: notificationHeading.applicationName
property string summary
property var time
property alias configurable: configureButton.visible
property alias dismissable: dismissButton.visible
property alias closable: closeButton.visible
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 string deviceName
property int jobState
property int percentage
......@@ -64,7 +65,7 @@ ColumnLayout {
property QtObject jobDetails
property bool showDetails
property string configureActionLabel
property alias configureActionLabel: notificationHeading.configureActionLabel
property var actionNames: []
property var actionLabels: []
......@@ -84,126 +85,21 @@ ColumnLayout {
signal resumeJobClicked
signal killJobClicked
onTimeChanged: ageLabel.updateText()
onTimeChanged: notificationHeading.updateAgoText()
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 {
NotificationHeader {
id: notificationHeading
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()
}
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
jobDetails: notificationItem.jobDetails
HeaderButton {
id: closeButton
tooltip: i18n("Close")
iconSource: "window-close"
visible: false
onClicked: notificationItem.closeClicked()
}
}
}
onConfigureClicked: notificationItem.configureClicked()
onDismissClicked: notificationItem.dismissClicked()
onCloseClicked: notificationItem.closeClicked()
}
// Notification body
......@@ -241,7 +137,7 @@ ColumnLayout {
// 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()
visible: text !== "" && text.toLocaleLowerCase().trim() !== notificationItem.applicationName.toLocaleLowerCase().trim()
PlasmaCore.ToolTipArea {
anchors.fill: parent
......@@ -274,7 +170,7 @@ ColumnLayout {
usesPlasmaTheme: false
smooth: true
// don't show two identical icons
visible: valid && source != applicationIconItem.source
visible: valid && source != notificationItem.applicationIconSource
}
}
......@@ -303,24 +199,28 @@ ColumnLayout {
}
}
// Notification actions
Flow { // it's a Flow so it can wrap if too long
RowLayout {
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)
// 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)
}
}
}
}
......
......@@ -72,7 +72,7 @@ MouseArea {
menuButton.checked = false;
fileMenu.visualParent = this;
fileMenu.show(mouse.x, mouse.y);
fileMenu.open(mouse.x, mouse.y);
}
}
onPositionChanged: {
......
......@@ -53,7 +53,7 @@ Item {
return lines.join("\n");
}
Plasmoid.switchWidth: units.gridUnit * 18
Plasmoid.switchWidth: units.gridUnit * 14
Plasmoid.switchHeight: units.gridUnit * 10
Plasmoid.compactRepresentation: CompactRepresentation {
......@@ -72,6 +72,7 @@ Item {
showExpired: true
showDismissed: true
showJobs: true
groupMode: NotificationManager.Notifications.GroupApplicationsFlat
}
function action_notificationskcm() {
......
......@@ -44,6 +44,8 @@ PlasmaCore.Dialog {
property alias body: notificationItem.body
property alias icon: notificationItem.icon
property alias urls: notificationItem.urls
property alias deviceName: notificationItem.deviceName
property int urgency
property int timeout
......@@ -115,8 +117,16 @@ PlasmaCore.Dialog {
onTriggered: 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: timeoutRect
id: timeoutIndicatorRect
anchors {
right: parent.right
rightMargin: -notificationPopup.margins.right
......@@ -126,7 +136,7 @@ PlasmaCore.Dialog {
width: units.devicePixelRatio * 3
radius: width
color: theme.highlightColor
opacity: timer.running ? 0.6 : 0
opacity: timeoutIndicatorAnimation.running ? 0.6 : 0
visible: units.longDuration > 1
Behavior on opacity {
NumberAnimation {
......@@ -135,12 +145,13 @@ PlasmaCore.Dialog {
}
NumberAnimation {
target: timeoutRect
id: timeoutIndicatorAnimation
target: timeoutIndicatorRect
property: "height"
from: area.height + notificationPopup.margins.top + notificationPopup.margins.bottom
to: 0
duration: timer.interval
running: timer.running && units.longDuration > 1
duration: Math.min(timer.interval, timeoutIndicatorDelayTimer.remainingTimeout)
running: timer.running && !timeoutIndicatorDelayTimer.running && units.longDuration > 1
}
}
......@@ -149,6 +160,7 @@ PlasmaCore.Dialog {
width: parent.width
hovered: area.containsMouse
maximumLineCount: 8 // TODO configurable?
bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0
thumbnailLeftPadding: -notificationPopup.margins.left
thumbnailRightPadding: -notificationPopup.margins.right
......
......@@ -79,6 +79,7 @@ QtObject {
showExpired: false
showDismissed: false
showJobs: true
groupMode: NotificationManager.Notifications.GroupDisabled
urgencies: NotificationManager.Notifications.NormalUrgency | NotificationManager.Notifications.CriticalUrgency
}
......@@ -175,10 +176,12 @@ QtObject {
summary: model.summary
body: model.body || "" // TODO
icon: model.image || model.iconName
urls: model.urls || []
hasDefaultAction: model.hasDefaultAction || false
timeout: model.timeout
urls: model.urls || []
urgency: model.urgency
deviceName: model.deviceName || ""
jobState: model.jobState || 0
percentage: model.percentage || 0
......
......@@ -14,8 +14,6 @@ ecm_qt_declare_logging_category(notifications_engine_SRCS HEADER debug.h
CATEGORY_NAME kde.dataengine.notifications`
DEFAULT_SEVERITY Info)
qt5_add_dbus_adaptor( notifications_engine_SRCS org.freedesktop.Notifications.xml notificationsengine.h NotificationsEngine )
add_library(plasma_engine_notifications MODULE ${notifications_engine_SRCS})
target_link_libraries(plasma_engine_notifications
......@@ -27,6 +25,7 @@ target_link_libraries(plasma_engine_notifications
<