Commit 9cee05eb authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Implement do not disturb mode and grouping collapse

- Rework compact representation animations a bit
  For some reason they often got stuck
- Implement do not disturb mode for applications
- Add fallback timeout to ensure notifications eventually timeout
  Otherwise when disabling dnd mode you will get spammed
  and we would also keep apps running indefinitely waiting for the notification to close
- Install plasmanotifyrc with some sane defaults
  So far only lets Spectacle show its screenshot notifications in dnd mode
- Introduce Closable role rather than hardcoding that behavior everywhere in the view
- Better app identification/grouping for jobs
- Be more lenient about app identification (firefox will match Firefox, too)
- Cleanups
parent 54b4fc09
...@@ -69,19 +69,6 @@ MouseArea { ...@@ -69,19 +69,6 @@ MouseArea {
elementId: "notification-disabled" elementId: "notification-disabled"
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Item { Item {
id: jobProgressItem id: jobProgressItem
anchors { anchors {
...@@ -112,6 +99,22 @@ MouseArea { ...@@ -112,6 +99,22 @@ MouseArea {
anchors.centerIn: parent anchors.centerIn: parent
font.pointSize: -1 font.pointSize: -1
font.pixelSize: Math.round(parent.height * 0.6) font.pixelSize: Math.round(parent.height * 0.6)
text: count || "" // don't show number zero
property int count: 0
onCountChanged: countChangeAnimation.start();
ParallelAnimation {
id: countChangeAnimation
NumberAnimation {
target: countLabel
property: "scale"
from: 2
to: 1
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
} }
PlasmaComponents.BusyIndicator { PlasmaComponents.BusyIndicator {
...@@ -130,42 +133,16 @@ MouseArea { ...@@ -130,42 +133,16 @@ MouseArea {
opacity: 0 opacity: 0
scale: 2 scale: 2
visible: opacity > 0 visible: opacity > 0
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
} }
} }
PlasmaCore.IconItem { PlasmaCore.IconItem {
id: dndIcon id: dndIcon
anchors.fill: parent anchors.fill: parent
source: "face-quiet" source: "notifications-disabled"
opacity: 0 opacity: 0
scale: 2 scale: 2
visible: opacity > 0 visible: opacity > 0
Behavior on scale {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
easing.type: Easing.InOutQuad
}
}
} }
states: [ states: [
...@@ -177,7 +154,7 @@ MouseArea { ...@@ -177,7 +154,7 @@ MouseArea {
} }
PropertyChanges { PropertyChanges {
target: countLabel target: countLabel
text: compactRoot.jobsCount count: compactRoot.jobsCount
} }
PropertyChanges { PropertyChanges {
target: busyIndicator target: busyIndicator
...@@ -217,7 +194,19 @@ MouseArea { ...@@ -217,7 +194,19 @@ MouseArea {
} }
PropertyChanges { PropertyChanges {
target: countLabel target: countLabel
text: compactRoot.unreadCount count: compactRoot.unreadCount
}
}
]
transitions: [
Transition {
to: "*" // any state
NumberAnimation {
targets: [notificationIcon, notificationActiveItem, dndIcon]
properties: "opacity,scale"
duration: units.longDuration
easing.type: Easing.InOutQuad
} }
} }
] ]
......
...@@ -75,17 +75,19 @@ ColumnLayout { ...@@ -75,17 +75,19 @@ ColumnLayout {
onClicked: { onClicked: {
if (Globals.inhibited) { if (Globals.inhibited) {
notificationSettings.notificationsInhibitedUntil = undefined; notificationSettings.notificationsInhibitedUntil = undefined;
notificationSettings.revokeApplicationInhibitions();
notificationSettings.save(); notificationSettings.save();
} }
} }
contentItem: RowLayout { contentItem: RowLayout {
spacing: dndCheck.spacing
PlasmaCore.IconItem { PlasmaCore.IconItem {
Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing
Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0 Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0
// FIXME proper icon source: "notifications-disabled"
source: "face-quiet"
Layout.preferredWidth: units.iconSizes.smallMedium Layout.preferredWidth: units.iconSizes.smallMedium
Layout.preferredHeight: units.iconSizes.smallMedium Layout.preferredHeight: units.iconSizes.smallMedium
} }
...@@ -144,6 +146,17 @@ ColumnLayout { ...@@ -144,6 +146,17 @@ ColumnLayout {
} }
PlasmaComponents.MenuItem { PlasmaComponents.MenuItem {
text: i18n("Until Monday") text: i18n("Until Monday")
// show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning"
visible: dndMenu.date.getDay() >= 5
readonly property date date: {
var d = dndMenu.date;
d.setHours(dndMorningHour);
// wraps around if neccessary
d.setDate(d.getDate() + (7 - d.getDay() + 1));
d.setMinutes(0);
d.setSeconds(0);
return d;
}
} }
} }
} }
...@@ -155,30 +168,50 @@ ColumnLayout { ...@@ -155,30 +168,50 @@ ColumnLayout {
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {
iconName: "configure" iconName: "configure"
tooltip: plasmoid.action("configure").text tooltip: plasmoid.action("openKcm").text
visible: plasmoid.action("configure").enabled visible: plasmoid.action("openKcm").enabled
onClicked: plasmoid.action("configure").trigger() onClicked: plasmoid.action("openKcm").trigger()
} }
} }
PlasmaExtras.DescriptiveLabel { PlasmaExtras.DescriptiveLabel {
leftPadding: dndCheck.mirrored ? 0 : units.smallSpacing + dndCheck.indicator.width + dndCheck.spacing Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium
rightPadding: dndCheck.mirrored ? units.smallSpacing + dndCheck.indicator.width + dndCheck.spacing : 0 Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium : 0
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
textFormat: Text.PlainText textFormat: Text.PlainText
text: { text: {
if (Globals.inhibited) { if (!Globals.inhibited) {
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil return "";
var inhibitedUntilValid = !isNaN(inhibitedUntil.getTime()); }
// TODO check app inhibition, too var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
if (inhibitedUntilValid) { var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication;
return i18nc("Do not disturb mode enabled until date", "Until %1",
KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat)); var sections = [];
if (!isNaN(inhibitedUntil.getTime())) {
sections.push(i18nc("Do not disturb until date", "Until %1",
KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat)));
}
if (inhibitedByApp) {
var inhibitionAppNames = notificationSettings.notificationInhibitionApplications;
var inhibitionAppReasons = notificationSettings.notificationInhibitionReasons;
for (var i = 0, length = inhibitionAppNames.length; i < length; ++i) {
var name = inhibitionAppNames[i];
var reason = inhibitionAppReasons[i];
if (reason) {
sections.push(i18nc("Do not disturb until app has finished (reason)", "While %1 is active (%2)", name, reason));
} else {
sections.push(i18nc("Do not disturb until app has finished", "While %1 is active", name));
}
} }
} }
return "";
return sections.join(" · ");
} }
visible: text !== "" visible: text !== ""
} }
...@@ -208,8 +241,8 @@ ColumnLayout { ...@@ -208,8 +241,8 @@ ColumnLayout {
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {
iconName: "edit-clear-history" iconName: "edit-clear-history"
tooltip: i18n("Clear History") tooltip: i18n("Clear History")
visible: historyModel.expiredNotificationsCount > 0 visible: plasmoid.action("clearHistory").visible
onClicked: historyModel.clear(NotificationManager.Notifications.ClearExpired) onClicked: action_clearHistory()
} }
} }
...@@ -249,11 +282,23 @@ ColumnLayout { ...@@ -249,11 +282,23 @@ ColumnLayout {
applicationName: model.applicationName applicationName: model.applicationName
applicationIconSource: model.applicationIconName applicationIconSource: model.applicationIconName
time: model.updated || model.created //time: model.updated || model.created
configurable: model.configurable configurable: model.configurable
closable: model.closable
expandable: true
expanded: model.isGroupExpanded
expandedCount: model.groupChildrenCount
collapsedCount: historyModel.groupLimit
onExpandClicked: {
if (expanded) {
historyModel.collapse(historyModel.index(index, 0));
} else {
historyModel.expand(historyModel.index(index, 0));
}
}
// FIXME close group
onCloseClicked: historyModel.close(historyModel.index(index, 0)) onCloseClicked: historyModel.close(historyModel.index(index, 0))
//onDismissClicked: model.dismissed = false //onDismissClicked: model.dismissed = false
// FIXME don't configure event but just app // FIXME don't configure event but just app
...@@ -269,7 +314,7 @@ ColumnLayout { ...@@ -269,7 +314,7 @@ ColumnLayout {
notificationType: model.type notificationType: model.type
headerVisible: !model.isInGroup inGroup: model.isInGroup
applicationName: model.applicationName applicationName: model.applicationName
applicatonIconSource: model.applicationIconName applicatonIconSource: model.applicationIconName
...@@ -283,8 +328,7 @@ ColumnLayout { ...@@ -283,8 +328,7 @@ ColumnLayout {
dismissable: model.type === NotificationManager.Notifications.JobType dismissable: model.type === NotificationManager.Notifications.JobType
&& model.jobState !== NotificationManager.Notifications.JobStateStopped && model.jobState !== NotificationManager.Notifications.JobStateStopped
&& model.dismissed && model.dismissed
closable: model.type === NotificationManager.Notifications.NotificationType closable: model.closable
|| model.jobState === NotificationManager.Notifications.JobStateStopped
summary: model.summary summary: model.summary
body: model.body || "" body: model.body || ""
...@@ -304,17 +348,21 @@ ColumnLayout { ...@@ -304,17 +348,21 @@ ColumnLayout {
// In the popup the default action is triggered by clicking on the popup // In the popup the default action is triggered by clicking on the popup
// however in the list this is undesirable, so instead show a clickable button // however in the list this is undesirable, so instead show a clickable button
// in case you have a non-expired notification in history (do not disturb mode) // in case you have a non-expired notification in history (do not disturb mode)
// unless it has the same label as an action
readonly property bool addDefaultAction: (model.hasDefaultAction
&& model.defaultActionLabel
&& (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false
actionNames: { actionNames: {
var actions = (model.actionNames || []); var actions = (model.actionNames || []);
if (model.hasDefaultAction) { if (addDefaultAction) {
actions.unshift("default"); // prepend actions.unshift("default"); // prepend
} }
return actions; return actions;
} }
actionLabels: { actionLabels: {
var labels = (model.actionLabels || []); var labels = (model.actionLabels || []);
if (model.hasDefaultAction) { if (addDefaultAction) {
labels.unshift(model.defaultActionLabel || i18n("Open")); // make sure it has some label labels.unshift(model.defaultActionLabel);
} }
return labels; return labels;
} }
......
...@@ -60,7 +60,7 @@ ColumnLayout { ...@@ -60,7 +60,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
textFormat: Text.PlainText textFormat: Text.PlainText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: jobItem.errorText || jobItem.jobDetails.text text: jobItem.errorText || (jobItem.jobDetails ? jobItem.jobDetails.text : "")
visible: text !== "" visible: text !== ""
} }
...@@ -134,6 +134,7 @@ ColumnLayout { ...@@ -134,6 +134,7 @@ ColumnLayout {
property var url: { property var url: {
if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped
|| jobItem.error || jobItem.error
|| !jobItem.jobDetails
|| jobItem.jobDetails.totalFiles <= 0) { || jobItem.jobDetails.totalFiles <= 0) {
return null; return null;
} }
...@@ -170,7 +171,7 @@ ColumnLayout { ...@@ -170,7 +171,7 @@ ColumnLayout {
PlasmaComponents.Button { PlasmaComponents.Button {
// would be nice to have the file icon here? // would be nice to have the file icon here?
text: jobItem.jobDetails.totalFiles > 1 ? i18n("Open Containing Folder") : i18n("Open") text: jobItem.jobDetails && jobItem.jobDetails.totalFiles > 1 ? i18n("Open Containing Folder") : i18n("Open")
onClicked: jobItem.openUrl(jobDoneActions.url) onClicked: jobItem.openUrl(jobDoneActions.url)
width: minimumWidth width: minimumWidth
} }
......
...@@ -28,7 +28,7 @@ ColumnLayout { ...@@ -28,7 +28,7 @@ ColumnLayout {
property alias notificationType: notificationItem.notificationType property alias notificationType: notificationItem.notificationType
property alias headerVisible: notificationItem.headerVisible property alias inGroup: notificationItem.inGroup
property alias applicationName: notificationItem.applicationName property alias applicationName: notificationItem.applicationName
property alias applicatonIconSource: notificationItem.applicationIconSource property alias applicatonIconSource: notificationItem.applicationIconSource
......
...@@ -34,15 +34,20 @@ import "global" ...@@ -34,15 +34,20 @@ import "global"
RowLayout { RowLayout {
id: notificationHeading id: notificationHeading
property bool inGroup
property int notificationType property int notificationType
property alias applicationIconSource: applicationIconItem.source property var applicationIconSource
property string applicationName property string applicationName
property string deviceName property string deviceName
property string configureActionLabel property string configureActionLabel
property alias expandable: expandButton.visible
property bool expanded
property int collapsedCount
property int expandedCount
property alias configurable: configureButton.visible property alias configurable: configureButton.visible
property alias dismissable: dismissButton.visible property alias dismissable: dismissButton.visible
property alias closable: closeButton.visible property alias closable: closeButton.visible
...@@ -52,6 +57,7 @@ RowLayout { ...@@ -52,6 +57,7 @@ RowLayout {
property int jobState property int jobState
property QtObject jobDetails property QtObject jobDetails
signal expandClicked
signal configureClicked signal configureClicked
signal dismissClicked signal dismissClicked
signal closeClicked signal closeClicked
...@@ -69,7 +75,6 @@ RowLayout { ...@@ -69,7 +75,6 @@ RowLayout {
Connections { Connections {
target: Globals target: Globals
// clock time changed // clock time changed
// TODO should we do this only when actually visible/expanded?
onTimeChanged: notificationHeading.updateAgoText() onTimeChanged: notificationHeading.updateAgoText()
} }
...@@ -77,6 +82,7 @@ RowLayout { ...@@ -77,6 +82,7 @@ RowLayout {
id: applicationIconItem id: applicationIconItem
Layout.preferredWidth: units.iconSizes.small Layout.preferredWidth: units.iconSizes.small
Layout.preferredHeight: units.iconSizes.small Layout.preferredHeight: units.iconSizes.small
source: !notificationHeading.inGroup ? notificationHeading.applicationIconSource : ""
usesPlasmaTheme: false usesPlasmaTheme: false
visible: valid visible: valid
} }
...@@ -96,7 +102,16 @@ RowLayout { ...@@ -96,7 +102,16 @@ RowLayout {
// updated periodically by a Timer hence this property with generate() function // updated periodically by a Timer hence this property with generate() function
property string agoText: "" property string agoText: ""
visible: text !== "" visible: text !== ""
text: generateRemainingText() || agoText text: {
if (expandable) {
if (expanded) {
return expandedCount;
} else {
return i18nc("n more notifications", "+%1", (expandedCount - collapsedCount));
}
}
return generateRemainingText() || agoText;
}
function generateAgoText() { function generateAgoText() {
if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) { if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) {
...@@ -175,6 +190,15 @@ RowLayout { ...@@ -175,6 +190,15 @@ RowLayout {
// These aren't ToolButtons so they can be perfectly aligned // These aren't ToolButtons so they can be perfectly aligned
// FIXME fix layout overlap // 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 { HeaderButton {
id: configureButton id: configureButton
tooltip: notificationHeading.configureActionLabel || i18n("Configure") tooltip: notificationHeading.configureActionLabel || i18n("Configure")
......
...@@ -37,7 +37,7 @@ ColumnLayout { ...@@ -37,7 +37,7 @@ ColumnLayout {
property int notificationType property int notificationType
property bool headerVisible: true property bool inGroup: false
property alias applicationIconSource: notificationHeading.applicationIconSource property alias applicationIconSource: notificationHeading.applicationIconSource
property alias applicationName: notificationHeading.applicationName property alias applicationName: notificationHeading.applicationName
...@@ -97,7 +97,8 @@ ColumnLayout { ...@@ -97,7 +97,8 @@ ColumnLayout {
id: notificationHeading id: notificationHeading
Layout.fillWidth: true Layout.fillWidth: true
visible: notificationItem.headerVisible inGroup: notificationItem.inGroup
parent: inGroup ? inGroupHeaderContainer : defaultHeaderContainer
notificationType: notificationItem.notificationType notificationType: notificationItem.notificationType
jobState: notificationItem.jobState jobState: notificationItem.jobState
...@@ -108,6 +109,11 @@ ColumnLayout { ...@@ -108,6 +109,11 @@ ColumnLayout {
onCloseClicked: notificationItem.closeClicked() onCloseClicked: notificationItem.closeClicked()
} }
RowLayout {
id: defaultHeaderContainer
Layout.fillWidth: true
}