diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml index a05076590..11bdd048f 100644 --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -1,561 +1,569 @@ /* * Copyright 2018-2019 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.8 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.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kcoreaddons 1.0 as KCoreAddons import org.kde.notificationmanager 1.0 as NotificationManager import "global" ColumnLayout{ // FIXME fix popup size when resizing panel smaller (so it collapses) //Layout.preferredWidth: units.gridUnit * 18 //Layout.preferredHeight: units.gridUnit * 24 //Layout.minimumWidth: units.gridUnit * 10 //Layout.minimumHeight: units.gridUnit * 15 Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical spacing: units.smallSpacing // TODO these should be configurable in the future readonly property int dndMorningHour: 6 readonly property int dndEveningHour: 20 // HACK forward focus to the list onActiveFocusChanged: { if (activeFocus) { list.forceActiveFocus(); } } Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded) { list.positionViewAtBeginning(); list.currentIndex = -1; } } } // header ColumnLayout { Layout.fillWidth: true spacing: 0 RowLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: dndRow spacing: units.smallSpacing PlasmaComponents3.CheckBox { id: dndCheck text: i18n("Do not disturb") spacing: units.smallSpacing checkable: true checked: Globals.inhibited // Let the menu open on press onPressed: { if (!Globals.inhibited) { dndMenu.date = new Date(); // shows ontop of CheckBox to hide the fact that it's unchecked // until you actually select something :) dndMenu.open(0, 0); } } // but disable only on click onClicked: { if (Globals.inhibited) { notificationSettings.notificationsInhibitedUntil = undefined; notificationSettings.revokeApplicationInhibitions(); + // overrules current mirrored screen setup, updates again when screen configuration changes + notificationSettings.screensMirrored = false; notificationSettings.save(); } } contentItem: RowLayout { spacing: dndCheck.spacing PlasmaCore.IconItem { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + dndCheck.spacing Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + dndCheck.spacing : 0 source: "notifications-disabled" Layout.preferredWidth: units.iconSizes.smallMedium Layout.preferredHeight: units.iconSizes.smallMedium } PlasmaComponents.Label { text: i18n("Do not disturb") } } PlasmaComponents.ModelContextMenu { id: dndMenu property date date visualParent: dndCheck onClicked: { notificationSettings.notificationsInhibitedUntil = model.date; notificationSettings.save(); } model: { var model = []; // For 1 hour var d = dndMenu.date; d.setHours(d.getHours() + 1); d.setSeconds(0); model.push({date: d, text: i18n("For 1 hour")}); d = dndMenu.date; d.setHours(d.getHours() + 4); d.setSeconds(0); model.push({date: d, text: i18n("For 4 hours")}); // Until this evening if (dndMenu.date.getHours() < dndEveningHour) { d = dndMenu.date; // TODO make the user's preferred time schedule configurable d.setHours(dndEveningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until this evening")}); } // Until next morning if (dndMenu.date.getHours() > dndMorningHour) { d = dndMenu.date; d.setDate(d.getDate() + 1); d.setHours(dndMorningHour); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until tomorrow morning")}); } // Until Monday // show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning" if (dndMenu.date.getDay() >= 5) { d = dndMenu.date; d.setHours(dndMorningHour); // wraps around if neccessary d.setDate(d.getDate() + (7 - d.getDay() + 1)); d.setMinutes(0); d.setSeconds(0); model.push({date: d, text: i18n("Until Monday")}); } // Until "turned off" d = dndMenu.date; // Just set it to one year in the future so we don't need yet another "do not disturb enabled" property d.setFullYear(d.getFullYear() + 1); model.push({date: d, text: i18n("Until turned off")}); return model; } } } } Item { Layout.fillWidth: true } PlasmaComponents.ToolButton { iconName: "configure" // remove mnemonics tooltip: plasmoid.action("openKcm").text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) { return p1.concat(p2, p3); }); visible: plasmoid.action("openKcm").enabled onClicked: plasmoid.action("openKcm").trigger() } PlasmaComponents.ToolButton { iconName: "edit-clear-history" tooltip: i18n("Clear History") enabled: plasmoid.action("clearHistory").visible onClicked: action_clearHistory() } } PlasmaExtras.DescriptiveLabel { Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + units.iconSizes.smallMedium : 0 Layout.fillWidth: true wrapMode: Text.WordWrap textFormat: Text.PlainText text: { if (!Globals.inhibited) { return ""; } var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication; + var inhibitedByMirroredScreens = notificationSettings.inhibitNotificationsWhenScreensMirrored + && notificationSettings.screensMirrored; var sections = []; // Show until time if valid but not if too far int he future if (!isNaN(inhibitedUntil.getTime()) && inhibitedUntil.getTime() - new Date().getTime() < 365 * 24 * 60 * 60 * 1000 /* 1 year*/) { 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)); } } } + if (inhibitedByMirroredScreens) { + sections.push(i18nc("Do not disturb because external mirrored screens connected", "Screens are mirrored")) + } + return sections.join(" ยท "); } visible: text !== "" } } PlasmaCore.SvgItem { elementId: "horizontal-line" Layout.fillWidth: true // why is this needed here but not in the delegate? Layout.preferredHeight: naturalSize.height svg: PlasmaCore.Svg { id: lineSvg imagePath: "widgets/line" } } // actual notifications PlasmaExtras.ScrollArea { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: units.gridUnit * 18 Layout.preferredHeight: units.gridUnit * 24 ListView { id: list model: historyModel currentIndex: -1 Keys.onDeletePressed: { var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { historyModel.close(idx); // TODO would be nice to stay inside the current group when deleting an item } } Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: { // Trigger default action, if any var idx = historyModel.index(currentIndex, 0); if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { historyModel.invokeDefaultAction(idx); return; } // Trigger thumbnail URL if there's one var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); if (urls && urls.length === 1) { Qt.openUrlExternally(urls[0]); historyModel.expire(idx); return; } // TODO for finished jobs trigger "Open" or "Open Containing Folder" action } Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) Keys.onPressed: { switch (event.key) { case Qt.Key_Home: currentIndex = 0; break; case Qt.Key_End: currentIndex = count - 1; break; } } function isRowExpanded(row) { var idx = historyModel.index(row, 0); return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); } function setGroupExpanded(row, expanded) { var rowIdx = historyModel.index(row, 0); var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); // If the current item went away when the group collapsed, scroll to the group heading if (!persistentRowIdx || !persistentRowIdx.valid) { if (persistentGroupIdx && persistentGroupIdx.valid) { list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); // When closed via keyboard, also set a sane current index if (list.currentIndex > -1) { list.currentIndex = persistentGroupIdx.row; } } } } highlightMoveDuration: 0 highlightResizeDuration: 0 // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus highlight: PlasmaCore.FrameSvgItem { imagePath: "widgets/listitem" prefix: "pressed" } add: Transition { SequentialAnimation { PropertyAction { property: "opacity"; value: 0 } PauseAnimation { duration: units.longDuration } ParallelAnimation { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: units.longDuration } NumberAnimation { property: "height"; from: 0; duration: units.longDuration } } } } addDisplaced: Transition { NumberAnimation { properties: "y"; duration: units.longDuration } } remove: Transition { ParallelAnimation { NumberAnimation { property: "opacity"; to: 0; duration: units.longDuration } NumberAnimation { property: "x"; to: list.width; duration: units.longDuration } } } removeDisplaced: Transition { SequentialAnimation { PauseAnimation { duration: units.longDuration } NumberAnimation { properties: "y"; duration: units.longDuration } } } // This is so the delegates can detect the change in "isInGroup" and show a separator section { property: "isInGroup" criteria: ViewSection.FullString } delegate: Loader { id: delegateLoader width: list.width sourceComponent: model.isGroup ? groupDelegate : notificationDelegate Component { id: groupDelegate NotificationHeader { applicationName: model.applicationName applicationIconSource: model.applicationIconName // don't show timestamp for group configurable: model.configurable closable: model.closable closeButtonTooltip: i18n("Close Group") onCloseClicked: { historyModel.close(historyModel.index(index, 0)) if (list.count === 0) { plasmoid.expanded = false; } } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) } } Component { id: notificationDelegate ColumnLayout { spacing: units.smallSpacing RowLayout { Item { id: groupLineContainer Layout.fillHeight: true Layout.topMargin: units.smallSpacing width: units.iconSizes.small visible: model.isInGroup PlasmaCore.SvgItem { elementId: "vertical-line" svg: lineSvg anchors.horizontalCenter: parent.horizontalCenter width: units.iconSizes.small height: parent.height } } NotificationItem { Layout.fillWidth: true notificationType: model.type inGroup: model.isInGroup applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" time: model.updated || model.created // configure button on every single notifications is bit overwhelming configurable: !inGroup && model.configurable dismissable: model.type === NotificationManager.Notifications.JobType && model.jobState !== NotificationManager.Notifications.JobStateStopped && model.dismissed // TODO would be nice to be able to undismiss jobs even when they autohide && notificationSettings.permanentJobPopups dismissed: model.dismissed || false closable: model.closable summary: model.summary body: model.body || "" icon: model.image || model.iconName urls: model.urls || [] jobState: model.jobState || 0 percentage: model.percentage || 0 jobError: model.jobError || 0 suspendable: !!model.suspendable killable: !!model.killable jobDetails: model.jobDetails || null configureActionLabel: model.configureActionLabel || "" // 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 // 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: { var actions = (model.actionNames || []); if (addDefaultAction) { actions.unshift("default"); // prepend } return actions; } actionLabels: { var labels = (model.actionLabels || []); if (addDefaultAction) { labels.unshift(model.defaultActionLabel); } return labels; } onCloseClicked: { historyModel.close(historyModel.index(index, 0)); if (list.count === 0) { plasmoid.expanded = false; } } onDismissClicked: { model.dismissed = false; plasmoid.expanded = false; } onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) onActionInvoked: { if (actionName === "default") { historyModel.invokeDefaultAction(historyModel.index(index, 0)); } else { historyModel.invokeAction(historyModel.index(index, 0), actionName); } // Keep it in the history historyModel.expire(historyModel.index(index, 0)); } onOpenUrl: { Qt.openUrlExternally(url); historyModel.expire(historyModel.index(index, 0)); } onFileActionInvoked: historyModel.expire(historyModel.index(index, 0)) onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) } } PlasmaComponents.ToolButton { Layout.preferredWidth: minimumWidth iconName: model.isGroupExpanded ? "arrow-up" : "arrow-down" text: model.isGroupExpanded ? i18n("Show Fewer") : i18nc("Expand to show n more notifications", "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) } PlasmaCore.SvgItem { Layout.fillWidth: true Layout.bottomMargin: units.smallSpacing elementId: "horizontal-line" svg: lineSvg // property is only atached to the delegate itself (the Loader in our case) visible: (!model.isInGroup || delegateLoader.ListView.nextSection !== delegateLoader.ListView.section) && delegateLoader.ListView.nextSection !== "" // don't show after last item } } } } PlasmaExtras.Heading { width: list.width level: 3 opacity: 0.6 text: i18n("No unread notifications.") visible: list.count === 0 } } } } diff --git a/applets/notifications/package/contents/ui/global/Globals.qml b/applets/notifications/package/contents/ui/global/Globals.qml index 68878497a..8837c6b9f 100644 --- a/applets/notifications/package/contents/ui/global/Globals.qml +++ b/applets/notifications/package/contents/ui/global/Globals.qml @@ -1,447 +1,451 @@ /* * Copyright 2019 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 */ pragma Singleton import QtQuick 2.8 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 Components import org.kde.kquickcontrolsaddons 2.0 import org.kde.notificationmanager 1.0 as NotificationManager import ".." // This singleton object contains stuff shared between all notification plasmoids, namely: // - Popup creation and placement // - Do not disturb mode QtObject { id: globals // Listened to by "ago" label in NotificationHeader to update all of them in unison signal timeChanged property bool inhibited: false onInhibitedChanged: { var pa = pulseAudio.item; if (!pa) { return; } var stream = pa.notificationStream; if (!stream) { return; } if (inhibited) { // Only remember that we muted if previously not muted. if (!stream.muted) { notificationSettings.notificationSoundsInhibited = true; stream.mute(); } } else { // Only unmute if we previously muted it. if (notificationSettings.notificationSoundsInhibited) { stream.unmute(); } notificationSettings.notificationSoundsInhibited = false; } notificationSettings.save(); } // 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] // HACK When a plasmoid is destroyed, QML sets its value to "null" in the Array // so we then remove it so we have a working "plasmoid" again onPlasmoidChanged: { if (!plasmoid) { // this doesn't emit a change, only in ratePlasmoids() it will detect the change plasmoids.splice(0, 1); // remove first ratePlasmoids(); } } // all notification plasmoids property var plasmoids: [] property int popupLocation: { switch (notificationSettings.popupPosition) { // Auto-determine location based on plasmoid location case NotificationManager.Settings.CloseToWidget: if (!plasmoid) { return Qt.AlignBottom | Qt.AlignRight; // just in case } var alignment = 0; if (plasmoid.location === PlasmaCore.Types.LeftEdge) { alignment |= Qt.AlignLeft; } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { alignment |= Qt.AlignRight; } else { // would be nice to do plasmoid.compactRepresentationItem.mapToItem(null) and then // position the popups depending on the relative position within the panel alignment |= Qt.application.layoutDirection === Qt.RightToLeft ? Qt.AlignLeft : Qt.AlignRight; } if (plasmoid.location === PlasmaCore.Types.TopEdge) { alignment |= Qt.AlignTop; } else { alignment |= Qt.AlignBottom; } return alignment; case NotificationManager.Settings.TopLeft: return Qt.AlignTop | Qt.AlignLeft; case NotificationManager.Settings.TopCenter: return Qt.AlignTop | Qt.AlignHCenter; case NotificationManager.Settings.TopRight: return Qt.AlignTop | Qt.AlignRight; case NotificationManager.Settings.BottomLeft: return Qt.AlignBottom | Qt.AlignLeft; case NotificationManager.Settings.BottomCenter: return Qt.AlignBottom | Qt.AlignHCenter; case NotificationManager.Settings.BottomRight: return Qt.AlignBottom | Qt.AlignRight; } } // The raw width of the popup's content item, the Dialog itself adds some margins property int popupWidth: units.gridUnit * 18 property int popupEdgeDistance: units.largeSpacing property int popupSpacing: units.largeSpacing // How much vertical screen real estate the notification popups may consume readonly property real popupMaximumScreenFill: 0.75 onPopupLocationChanged: Qt.callLater(positionPopups) Component.onCompleted: checkInhibition() function adopt(plasmoid) { // this doesn't emit a change, only in ratePlasmoids() it will detect the change globals.plasmoids.push(plasmoid); ratePlasmoids(); } // Sorts plasmoids based on a heuristic to find a suitable plasmoid to follow when placing popups function ratePlasmoids() { var plasmoidScore = function(plasmoid) { if (!plasmoid) { return 0; } var score = 0; // Prefer plasmoids in a panel, prefer horizontal panels over vertical ones if (plasmoid.location === PlasmaCore.Types.LeftEdge || plasmoid.location === PlasmaCore.Types.RightEdge) { score += 1; } else if (plasmoid.location === PlasmaCore.Types.TopEdge || plasmoid.location === PlasmaCore.Types.BottomEdge) { score += 2; } // Prefer iconified plasmoids if (!plasmoid.expanded) { ++score; } // Prefer plasmoids on primary screen if (plasmoid.nativeInterface && plasmoid.nativeInterface.isPrimaryScreen(plasmoid.screenGeometry)) { ++score; } return score; } var newPlasmoids = plasmoids; newPlasmoids.sort(function (a, b) { var scoreA = plasmoidScore(a); var scoreB = plasmoidScore(b); // Sort descending by score if (scoreA < scoreB) { return 1; } else if (scoreA > scoreB) { return -1; } else { return 0; } }); globals.plasmoids = newPlasmoids; } function checkInhibition() { globals.inhibited = Qt.binding(function() { var inhibited = false; var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; if (!isNaN(inhibitedUntil.getTime())) { inhibited |= (new Date().getTime() < inhibitedUntil.getTime()); } if (notificationSettings.notificationsInhibitedByApplication) { inhibited |= true; } + if (notificationSettings.inhibitNotificationsWhenScreensMirrored) { + inhibited |= notificationSettings.screensMirrored; + } + return inhibited; }); } function positionPopups() { if (!plasmoid) { return; } var screenRect = Qt.rect(plasmoid.screenGeometry.x + plasmoid.availableScreenRect.x, plasmoid.screenGeometry.y + plasmoid.availableScreenRect.y, plasmoid.availableScreenRect.width, plasmoid.availableScreenRect.height); if (screenRect.width <= 0 || screenRect.height <= 0) { return; } var y = screenRect.y; if (popupLocation & Qt.AlignBottom) { y += screenRect.height; } else { y += popupEdgeDistance; } var x = screenRect.x; if (popupLocation & Qt.AlignLeft) { x += popupEdgeDistance; } for (var i = 0; i < popupInstantiator.count; ++i) { var popup = popupInstantiator.objectAt(i); // Popup width is fixed, so don't rely on the actual window size var popupEffectiveWidth = popupWidth + popup.margins.left + popup.margins.right; if (popupLocation & Qt.AlignHCenter) { popup.x = x + (screenRect.width - popupEffectiveWidth) / 2; } else if (popupLocation & Qt.AlignRight) { popup.x = x + screenRect.width - popupEdgeDistance - popupEffectiveWidth; } else { popup.x = x; } // If the popup isn't ready yet, ignore its occupied space for now. // We'll reposition everything in onHeightChanged eventually. var delta = popup.height + (popup.height > 0 ? popupSpacing : 0); if (popupLocation & Qt.AlignTop) { popup.y = y; y += delta; } else { y -= delta; popup.y = y; } // don't let notifications take more than popupMaximumScreenFill of the screen var visible = true; if (i > 0) { // however always show at least one popup if (popupLocation & Qt.AlignTop) { visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill)); } else { visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill))); } } // TODO would be nice to hide popups when systray or panel controller is open popup.visible = visible; } } property QtObject popupNotificationsModel: NotificationManager.Notifications { limit: plasmoid ? (Math.ceil(plasmoid.availableScreenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0 showExpired: false showDismissed: false blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : [] whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : [] showJobs: notificationSettings.jobsInNotifications sortMode: NotificationManager.Notifications.SortByTypeAndUrgency groupMode: NotificationManager.Notifications.GroupDisabled urgencies: { var urgencies = 0; // Critical always except in do not disturb mode when disabled in settings if (!globals.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) { urgencies |= NotificationManager.Notifications.CriticalUrgency; } // Normal only when not in do not disturb mode if (!globals.inhibited) { urgencies |= NotificationManager.Notifications.NormalUrgency; } // Low only when enabled in settings and not in do not disturb mode if (!globals.inhibited && notificationSettings.lowPriorityPopups) { urgencies |=NotificationManager.Notifications.LowUrgency; } return urgencies; } } property QtObject notificationSettings: NotificationManager.Settings { onNotificationsInhibitedUntilChanged: globals.checkInhibition() } // This periodically checks whether do not disturb mode timed out and updates the "minutes ago" labels property QtObject timeSource: PlasmaCore.DataSource { engine: "time" connectedSources: ["Local"] interval: 60000 // 1 min intervalAlignment: PlasmaCore.Types.AlignToMinute onDataChanged: { checkInhibition(); globals.timeChanged(); } } property Instantiator popupInstantiator: Instantiator { model: popupNotificationsModel delegate: NotificationPopup { // so Instantiator can access that after the model row is gone readonly property var notificationId: model.notificationId popupWidth: globals.popupWidth type: model.urgency === NotificationManager.Notifications.CriticalUrgency && notificationSettings.keepCriticalAlwaysOnTop ? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification notificationType: model.type applicationName: model.applicationName applicationIconSource: model.applicationIconName originName: model.originName || "" time: model.updated || model.created configurable: model.configurable // For running jobs instead of offering a "close" button that might lead the user to // think that will cancel the job, we offer a "dismiss" button that hides it in the history dismissable: model.type === NotificationManager.Notifications.JobType && model.jobState !== NotificationManager.Notifications.JobStateStopped // TODO would be nice to be able to "pin" jobs when they autohide && notificationSettings.permanentJobPopups closable: model.closable summary: model.summary body: model.body || "" icon: model.image || model.iconName hasDefaultAction: model.hasDefaultAction || false timeout: model.timeout // Increase default timeout for notifications with a URL so you have enough time // to interact with the thumbnail or bring the window to the front where you want to drag it into defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0) // When configured to not keep jobs open permanently, we autodismiss them after the standard timeout dismissTimeout: !notificationSettings.permanentJobPopups && model.type === NotificationManager.Notifications.JobType && model.jobState !== NotificationManager.Notifications.JobStateStopped ? defaultTimeout : 0 urls: model.urls || [] urgency: model.urgency || NotificationManager.Notifications.NormalUrgency jobState: model.jobState || 0 percentage: model.percentage || 0 jobError: model.jobError || 0 suspendable: !!model.suspendable killable: !!model.killable jobDetails: model.jobDetails || null configureActionLabel: model.configureActionLabel || "" actionNames: model.actionNames actionLabels: model.actionLabels onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0)) onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) onDismissClicked: model.dismissed = true onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0)) onDefaultActionInvoked: { popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0)) popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) } onActionInvoked: { popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName) popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) } onOpenUrl: { Qt.openUrlExternally(url); popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) } onFileActionInvoked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) onSuspendJobClicked: popupNotificationsModel.suspendJob(popupNotificationsModel.index(index, 0)) onResumeJobClicked: popupNotificationsModel.resumeJob(popupNotificationsModel.index(index, 0)) onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0)) // popup width is fixed onHeightChanged: positionPopups() Component.onCompleted: { // Register apps that were seen spawning a popup so they can be configured later // Apps with notifyrc can already be configured anyway if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry && !model.notifyRcName) { notificationSettings.registerKnownApplication(model.desktopEntry); notificationSettings.save(); } // Tell the model that we're handling the timeout now popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0)); } } onObjectAdded: { positionPopups(); object.visible = true; } onObjectRemoved: { var notificationId = object.notificationId // Popup might have been destroyed because of a filter change, tell the model to do the timeout work for us again // cannot use QModelIndex here as the model row is already gone popupNotificationsModel.startTimeout(notificationId); positionPopups(); } } // TODO use pulseaudio-qt for this once it becomes a framework property QtObject pulseAudio: Loader { source: "PulseAudio.qml" } property Connections screenWatcher: Connections { target: plasmoid onAvailableScreenRectChanged: repositionTimer.start() onScreenGeometryChanged: repositionTimer.start() } // Normally popups are repositioned through Qt.callLater but in case of e.g. screen geometry changes we want to compress that property Timer repositionTimer: Timer { interval: 250 onTriggered: positionPopups() } } diff --git a/libnotificationmanager/CMakeLists.txt b/libnotificationmanager/CMakeLists.txt index 1139cacf5..3419c39de 100644 --- a/libnotificationmanager/CMakeLists.txt +++ b/libnotificationmanager/CMakeLists.txt @@ -1,121 +1,123 @@ add_definitions(-DTRANSLATION_DOMAIN=\"libnotificationmanager\") add_subdirectory(declarative) if(BUILD_TESTING) add_subdirectory(autotests) endif() set(notificationmanager_LIB_SRCS server.cpp server_p.cpp settings.cpp + mirroredscreenstracker.cpp notifications.cpp notification.cpp notificationsmodel.cpp notificationfilterproxymodel.cpp notificationsortproxymodel.cpp notificationgroupingproxymodel.cpp notificationgroupcollapsingproxymodel.cpp jobsmodel.cpp jobsmodel_p.cpp job.cpp job_p.cpp limitedrowcountproxymodel.cpp utils.cpp ) ecm_qt_declare_logging_category(notificationmanager_LIB_SRCS HEADER debug.h IDENTIFIER NOTIFICATIONMANAGER CATEGORY_NAME org.kde.plasma.notifications) if (${ECM_VERSION} STRGREATER "5.58.0") install(FILES libnotificationmanager.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) else() install(FILES libnotificationmanager.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() # Settings kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/donotdisturbsettings.kcfgc) kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/notificationsettings.kcfgc) kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/jobsettings.kcfgc) kconfig_add_kcfg_files(notificationmanager_LIB_SRCS kcfg/badgesettings.kcfgc) # DBus # Notifications qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.freedesktop.Notifications.xml server_p.h NotificationManager::ServerPrivate) # JobView qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.kuiserver.xml jobsmodel_p.h NotificationManager::JobsModelPrivate) qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.JobViewServer.xml jobsmodel_p.h NotificationManager::JobsModelPrivate) qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.JobViewServerV2.xml jobsmodel_p.h NotificationManager::JobsModelPrivate) qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.JobViewV2.xml job_p.h NotificationManager::JobPrivate) qt5_add_dbus_adaptor(notificationmanager_LIB_SRCS dbus/org.kde.JobViewV3.xml job_p.h NotificationManager::JobPrivate) add_library(notificationmanager ${notificationmanager_LIB_SRCS}) add_library(PW::LibNotificationManager ALIAS notificationmanager) target_compile_definitions(notificationmanager PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}") generate_export_header(notificationmanager) target_include_directories(notificationmanager PUBLIC "$" "$") target_link_libraries(notificationmanager PUBLIC Qt5::Core Qt5::Gui Qt5::Quick KF5::ConfigCore KF5::ItemModels PRIVATE Qt5::DBus KF5::ConfigGui KF5::I18n KF5::IconThemes KF5::KIOFileWidgets KF5::Plasma KF5::ProcessCore + KF5::Screen KF5::Service ) set_target_properties(notificationmanager PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 EXPORT_NAME LibNotificationManager) install(TARGETS notificationmanager EXPORT notificationmanagerLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES server.h notifications.h notification.h jobsmodel.h job.h settings.h ${CMAKE_CURRENT_BINARY_DIR}/notificationmanager_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/notificationmanager COMPONENT Devel ) write_basic_config_version_file(${CMAKE_CURRENT_BINARY_DIR}/LibNotificationManagerConfigVersion.cmake VERSION "${PROJECT_VERSION}" COMPATIBILITY AnyNewerVersion) set(CMAKECONFIG_INSTALL_DIR ${KDE_INSTALL_LIBDIR}/cmake/LibNotificationManager) configure_package_config_file(LibNotificationManagerConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/LibNotificationManagerConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/LibNotificationManagerConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/LibNotificationManagerConfigVersion.cmake DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(EXPORT notificationmanagerLibraryTargets NAMESPACE PW:: DESTINATION ${CMAKECONFIG_INSTALL_DIR} FILE LibNotificationManagerLibraryTargets.cmake ) install(FILES plasmanotifyrc DESTINATION ${KDE_INSTALL_CONFDIR}) diff --git a/libnotificationmanager/kcfg/donotdisturbsettings.kcfg b/libnotificationmanager/kcfg/donotdisturbsettings.kcfg index 90436f267..786d55ac7 100644 --- a/libnotificationmanager/kcfg/donotdisturbsettings.kcfg +++ b/libnotificationmanager/kcfg/donotdisturbsettings.kcfg @@ -1,17 +1,21 @@ + + true + + false diff --git a/libnotificationmanager/mirroredscreenstracker.cpp b/libnotificationmanager/mirroredscreenstracker.cpp new file mode 100644 index 000000000..56d19bd52 --- /dev/null +++ b/libnotificationmanager/mirroredscreenstracker.cpp @@ -0,0 +1,100 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 6 of version 3 of the license. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "mirroredscreenstracker_p.h" + +#include + +#include +#include +#include + +#include "debug.h" + +using namespace NotificationManager; + +MirroredScreensTracker::MirroredScreensTracker() + : QObject(nullptr) +{ + connect(new KScreen::GetConfigOperation(KScreen::GetConfigOperation::NoEDID), &KScreen::ConfigOperation::finished, this, + [this](KScreen::ConfigOperation *op) { + m_screenConfiguration = qobject_cast(op)->config(); + checkScreensMirrored(); + + KScreen::ConfigMonitor::instance()->addConfig(m_screenConfiguration); + connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, + this, &MirroredScreensTracker::checkScreensMirrored); + }); +} + +MirroredScreensTracker::~MirroredScreensTracker() = default; + +MirroredScreensTracker::Ptr MirroredScreensTracker::createTracker() +{ + static QWeakPointer s_instance; + if (!s_instance) { + QSharedPointer ptr(new MirroredScreensTracker()); + s_instance = ptr.toWeakRef(); + return ptr; + } + return s_instance.toStrongRef(); +} + +bool MirroredScreensTracker::screensMirrored() const +{ + return m_screensMirrored; +} + +void MirroredScreensTracker::setScreensMirrored(bool mirrored) +{ + if (m_screensMirrored != mirrored) { + m_screensMirrored = mirrored; + emit screensMirroredChanged(mirrored); + } +} + +void MirroredScreensTracker::checkScreensMirrored() +{ + if (!m_screenConfiguration) { + setScreensMirrored(false); + return; + } + + const auto outputs = m_screenConfiguration->outputs(); + for (const KScreen::OutputPtr &output : outputs) { + if (!output->isConnected() || !output->isEnabled()) { + continue; + } + + for (const KScreen::OutputPtr &checkOutput : outputs) { + if (checkOutput == output) { + continue; + } + + if (output->geometry().intersects(checkOutput->geometry())) { + qCDebug(NOTIFICATIONMANAGER) << "Screen geometry" << checkOutput->geometry() << "intersects" << output->geometry() << "- considering them to be mirrored"; + setScreensMirrored(true); + return; + } + } + } + + setScreensMirrored(false); +} diff --git a/libnotificationmanager/mirroredscreenstracker_p.h b/libnotificationmanager/mirroredscreenstracker_p.h new file mode 100644 index 000000000..4eeedeff1 --- /dev/null +++ b/libnotificationmanager/mirroredscreenstracker_p.h @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 6 of version 3 of the license. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include +#include + +#include + +namespace NotificationManager +{ + +/** + * @short Tracks whether there are any mirrored screens + * + * @author Kai Uwe Broulik + **/ +class MirroredScreensTracker : public QObject +{ + Q_OBJECT + +public: + ~MirroredScreensTracker(); + + using Ptr = QSharedPointer; + static Ptr createTracker(); + + bool screensMirrored() const; + /** + * Set whether screens are mirrored + * + * This is public so that automatic do not disturb mode when screens are mirrored + * can be disabled temporarily until screen configuration changes again. + */ + void setScreensMirrored(bool mirrored); + Q_SIGNAL void screensMirroredChanged(bool mirrored); + +private: + MirroredScreensTracker(); + Q_DISABLE_COPY(MirroredScreensTracker) + + void checkScreensMirrored(); + + KScreen::ConfigPtr m_screenConfiguration; + bool m_screensMirrored = false; + +}; + +} // namespace NotificationManager diff --git a/libnotificationmanager/settings.cpp b/libnotificationmanager/settings.cpp index 6ce211466..fa3495908 100644 --- a/libnotificationmanager/settings.cpp +++ b/libnotificationmanager/settings.cpp @@ -1,562 +1,618 @@ /* * Copyright 2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "settings.h" #include #include #include #include "server.h" +#include "mirroredscreenstracker_p.h" #include "debug.h" // Settings #include "donotdisturbsettings.h" #include "notificationsettings.h" #include "jobsettings.h" #include "badgesettings.h" using namespace NotificationManager; class Q_DECL_HIDDEN Settings::Private { public: explicit Private(Settings *q); ~Private(); void setDirty(bool dirty); Settings::NotificationBehaviors groupBehavior(const KConfigGroup &group) const; void setGroupBehavior(KConfigGroup &group, const Settings::NotificationBehaviors &behavior); KConfigGroup servicesGroup() const; KConfigGroup applicationsGroup() const; QStringList behaviorMatchesList(const KConfigGroup &group, Settings::NotificationBehavior behavior, bool on) const; Settings *q; KSharedConfig::Ptr config; KConfigWatcher::Ptr watcher; QMetaObject::Connection watcherConnection; + MirroredScreensTracker::Ptr mirroredScreensTracker; + bool live = false; // set to true initially in constructor bool dirty = false; }; Settings::Private::Private(Settings *q) : q(q) { } Settings::Private::~Private() = default; void Settings::Private::setDirty(bool dirty) { if (this->dirty != dirty) { this->dirty = dirty; emit q->dirtyChanged(); } } Settings::NotificationBehaviors Settings::Private::groupBehavior(const KConfigGroup &group) const { Settings::NotificationBehaviors behaviors; behaviors.setFlag(Settings::ShowPopups, group.readEntry("ShowPopups", true)); // show popups in dnd mode implies the show popups behaviors.setFlag(Settings::ShowPopupsInDoNotDisturbMode, behaviors.testFlag(Settings::ShowPopups) && group.readEntry("ShowPopupsInDndMode", false)); behaviors.setFlag(Settings::ShowInHistory, group.readEntry("ShowInHistory", true)); behaviors.setFlag(Settings::ShowBadges, group.readEntry("ShowBadges", true)); return behaviors; } void Settings::Private::setGroupBehavior(KConfigGroup &group, const Settings::NotificationBehaviors &behavior) { if (groupBehavior(group) == behavior) { return; } const bool showPopups = behavior.testFlag(Settings::ShowPopups); if (showPopups && !group.hasDefault("ShowPopups")) { group.revertToDefault("ShowPopups", KConfigBase::Notify); } else { group.writeEntry("ShowPopups", showPopups, KConfigBase::Notify); } const bool showPopupsInDndMode = behavior.testFlag(Settings::ShowPopupsInDoNotDisturbMode); if (!showPopupsInDndMode && !group.hasDefault("ShowPopupsInDndMode")) { group.revertToDefault("ShowPopupsInDndMode", KConfigBase::Notify); } else { group.writeEntry("ShowPopupsInDndMode", showPopupsInDndMode, KConfigBase::Notify); } const bool showInHistory = behavior.testFlag(Settings::ShowInHistory); if (showInHistory && !group.hasDefault("ShowInHistory")) { group.revertToDefault("ShowInHistory", KConfig::Notify); } else { group.writeEntry("ShowInHistory", showInHistory, KConfigBase::Notify); } const bool showBadges = behavior.testFlag(Settings::ShowBadges); if (showBadges && !group.hasDefault("ShowBadges")) { group.revertToDefault("ShowBadges", KConfigBase::Notify); } else { group.writeEntry("ShowBadges", showBadges, KConfigBase::Notify); } setDirty(true); } KConfigGroup Settings::Private::servicesGroup() const { return config->group("Services"); } KConfigGroup Settings::Private::applicationsGroup() const { return config->group("Applications"); } QStringList Settings::Private::behaviorMatchesList(const KConfigGroup &group, Settings::NotificationBehavior behavior, bool on) const { QStringList matches; const QStringList apps = group.groupList(); for (const QString &app : apps) { if (groupBehavior(group.group(app)).testFlag(behavior) == on) { matches.append(app); } } return matches; } Settings::Settings(QObject *parent) // FIXME static thing for config file name : Settings(KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc")), parent) { } Settings::Settings(const KSharedConfig::Ptr &config, QObject *parent) : QObject(parent) , d(new Private(this)) { d->config = config; static bool s_settingsInited = false; if (!s_settingsInited) { DoNotDisturbSettings::instance(config); NotificationSettings::instance(config); JobSettings::instance(config); BadgeSettings::instance(config); s_settingsInited = true; } setLive(true); connect(&Server::self(), &Server::inhibitedChanged, this, &Settings::notificationsInhibitedByApplicationChanged); connect(&Server::self(), &Server::inhibitionApplicationsChanged, this, &Settings::notificationInhibitionApplicationsChanged); + + if (DoNotDisturbSettings::whenScreensMirrored()) { + d->mirroredScreensTracker = MirroredScreensTracker::createTracker(); + connect(d->mirroredScreensTracker.data(), &MirroredScreensTracker::screensMirroredChanged, this, &Settings::screensMirroredChanged); + } } Settings::~Settings() { d->config->markAsClean(); } Settings::NotificationBehaviors Settings::applicationBehavior(const QString &desktopEntry) const { return d->groupBehavior(d->applicationsGroup().group(desktopEntry)); } void Settings::setApplicationBehavior(const QString &desktopEntry, NotificationBehaviors behaviors) { KConfigGroup group(d->applicationsGroup().group(desktopEntry)); d->setGroupBehavior(group, behaviors); } Settings::NotificationBehaviors Settings::serviceBehavior(const QString ¬ifyRcName) const { return d->groupBehavior(d->servicesGroup().group(notifyRcName)); } void Settings::setServiceBehavior(const QString ¬ifyRcName, NotificationBehaviors behaviors) { KConfigGroup group(d->servicesGroup().group(notifyRcName)); d->setGroupBehavior(group, behaviors); } void Settings::registerKnownApplication(const QString &desktopEntry) { KService::Ptr service = KService::serviceByDesktopName(desktopEntry); if (!service) { qCDebug(NOTIFICATIONMANAGER) << "Application" << desktopEntry << "cannot be registered as seen application since there is no service for it"; return; } if (service->noDisplay()) { qCDebug(NOTIFICATIONMANAGER) << "Application" << desktopEntry << "will not be registered as seen application since it's marked as NoDisplay"; return; } if (knownApplications().contains(desktopEntry)) { return; } d->applicationsGroup().group(desktopEntry).writeEntry("Seen", true); emit knownApplicationsChanged(); } void Settings::forgetKnownApplication(const QString &desktopEntry) { if (!knownApplications().contains(desktopEntry)) { return; } // Only remove applications that were added through registerKnownApplication if (!d->applicationsGroup().group(desktopEntry).readEntry("Seen", false)) { qCDebug(NOTIFICATIONMANAGER) << "Application" << desktopEntry << "will not be removed from seen applications since it wasn't one."; return; } d->applicationsGroup().deleteGroup(desktopEntry); emit knownApplicationsChanged(); } void Settings::load() { d->config->markAsClean(); d->config->reparseConfiguration(); DoNotDisturbSettings::self()->load(); NotificationSettings::self()->load(); JobSettings::self()->load(); BadgeSettings::self()->load(); emit settingsChanged(); d->setDirty(false); } void Settings::save() { DoNotDisturbSettings::self()->save(); NotificationSettings::self()->save(); JobSettings::self()->save(); BadgeSettings::self()->save(); d->config->sync(); d->setDirty(false); } void Settings::defaults() { DoNotDisturbSettings::self()->setDefaults(); NotificationSettings::self()->setDefaults(); JobSettings::self()->setDefaults(); BadgeSettings::self()->setDefaults(); } bool Settings::live() const { return d->live; } void Settings::setLive(bool live) { if (live == d->live) { return; } d->live = live; if (live) { d->watcher = KConfigWatcher::create(d->config); d->watcherConnection = connect(d->watcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { Q_UNUSED(names); if (group.name() == QLatin1String("DoNotDisturb")) { DoNotDisturbSettings::self()->load(); + + bool emitScreensMirroredChanged = false; + if (DoNotDisturbSettings::whenScreensMirrored()) { + if (!d->mirroredScreensTracker) { + d->mirroredScreensTracker = MirroredScreensTracker::createTracker(); + emitScreensMirroredChanged = d->mirroredScreensTracker->screensMirrored(); + connect(d->mirroredScreensTracker.data(), &MirroredScreensTracker::screensMirroredChanged, this, &Settings::screensMirroredChanged); + } + } else if (d->mirroredScreensTracker) { + emitScreensMirroredChanged = d->mirroredScreensTracker->screensMirrored(); + d->mirroredScreensTracker.reset(); + } + + if (emitScreensMirroredChanged) { + emit screensMirroredChanged(); + } } else if (group.name() == QLatin1String("Notifications")) { NotificationSettings::self()->load(); } else if (group.name() == QLatin1String("Jobs")) { JobSettings::self()->load(); } else if (group.name() == QLatin1String("Badges")) { BadgeSettings::self()->load(); } emit settingsChanged(); }); } else { disconnect(d->watcherConnection); d->watcherConnection = QMetaObject::Connection(); d->watcher.reset(); } emit liveChanged(); } bool Settings::dirty() const { // KConfigSkeleton doesn't write into the KConfig until calling save() // so we need to track d->config->isDirty() manually return d->dirty; } bool Settings::keepCriticalAlwaysOnTop() const { return NotificationSettings::criticalAlwaysOnTop(); } void Settings::setKeepCriticalAlwaysOnTop(bool enable) { if (this->keepCriticalAlwaysOnTop() == enable) { return; } NotificationSettings::setCriticalAlwaysOnTop(enable); d->setDirty(true); } bool Settings::criticalPopupsInDoNotDisturbMode() const { return NotificationSettings::criticalInDndMode(); } void Settings::setCriticalPopupsInDoNotDisturbMode(bool enable) { if (this->criticalPopupsInDoNotDisturbMode() == enable) { return; } NotificationSettings::setCriticalInDndMode(enable); d->setDirty(true); } bool Settings::lowPriorityPopups() const { return NotificationSettings::lowPriorityPopups(); } void Settings::setLowPriorityPopups(bool enable) { if (this->lowPriorityPopups() == enable) { return; } NotificationSettings::setLowPriorityPopups(enable); d->setDirty(true); } bool Settings::lowPriorityHistory() const { return NotificationSettings::lowPriorityHistory(); } void Settings::setLowPriorityHistory(bool enable) { if (this->lowPriorityHistory() == enable) { return; } NotificationSettings::setLowPriorityHistory(enable); d->setDirty(true); } Settings::PopupPosition Settings::popupPosition() const { return NotificationSettings::popupPosition(); } void Settings::setPopupPosition(Settings::PopupPosition position) { if (this->popupPosition() == position) { return; } NotificationSettings::setPopupPosition(position); d->setDirty(true); } int Settings::popupTimeout() const { return NotificationSettings::popupTimeout(); } void Settings::setPopupTimeout(int timeout) { if (this->popupTimeout() == timeout) { return; } NotificationSettings::setPopupTimeout(timeout); d->setDirty(true); } void Settings::resetPopupTimeout() { setPopupTimeout(NotificationSettings::defaultPopupTimeoutValue()); } bool Settings::jobsInTaskManager() const { return JobSettings::inTaskManager(); } void Settings::setJobsInTaskManager(bool enable) { if (jobsInTaskManager() == enable) { return; } JobSettings::setInTaskManager(enable); d->setDirty(true); } bool Settings::jobsInNotifications() const { return JobSettings::inNotifications(); } void Settings::setJobsInNotifications(bool enable) { if (jobsInNotifications() == enable) { return; } JobSettings::setInNotifications(enable); d->setDirty(true); } bool Settings::permanentJobPopups() const { return JobSettings::permanentPopups(); } void Settings::setPermanentJobPopups(bool enable) { if (permanentJobPopups() == enable) { return; } JobSettings::setPermanentPopups(enable); d->setDirty(true); } bool Settings::badgesInTaskManager() const { return BadgeSettings::inTaskManager(); } void Settings::setBadgesInTaskManager(bool enable) { if (badgesInTaskManager() == enable) { return; } BadgeSettings::setInTaskManager(enable); d->setDirty(true); } QStringList Settings::knownApplications() const { return d->applicationsGroup().groupList(); } QStringList Settings::popupBlacklistedApplications() const { return d->behaviorMatchesList(d->applicationsGroup(), ShowPopups, false); } QStringList Settings::popupBlacklistedServices() const { return d->behaviorMatchesList(d->servicesGroup(), ShowPopups, false); } QStringList Settings::doNotDisturbPopupWhitelistedApplications() const { return d->behaviorMatchesList(d->applicationsGroup(), ShowPopupsInDoNotDisturbMode, true); } QStringList Settings::doNotDisturbPopupWhitelistedServices() const { return d->behaviorMatchesList(d->servicesGroup(), ShowPopupsInDoNotDisturbMode, true); } QStringList Settings::historyBlacklistedApplications() const { return d->behaviorMatchesList(d->applicationsGroup(), ShowInHistory, false); } QStringList Settings::historyBlacklistedServices() const { return d->behaviorMatchesList(d->servicesGroup(), ShowInHistory, false); } QStringList Settings::badgeBlacklistedApplications() const { return d->behaviorMatchesList(d->applicationsGroup(), ShowBadges, false); } QDateTime Settings::notificationsInhibitedUntil() const { return DoNotDisturbSettings::until(); } void Settings::setNotificationsInhibitedUntil(const QDateTime &time) { DoNotDisturbSettings::setUntil(time); d->setDirty(true); } void Settings::resetNotificationsInhibitedUntil() { setNotificationsInhibitedUntil(QDateTime());// FIXME DoNotDisturbSettings::defaultUntilValue()); } bool Settings::notificationsInhibitedByApplication() const { return Server::self().inhibited(); } QStringList Settings::notificationInhibitionApplications() const { return Server::self().inhibitionApplications(); } QStringList Settings::notificationInhibitionReasons() const { return Server::self().inhibitionReasons(); } +bool Settings::inhibitNotificationsWhenScreensMirrored() const +{ + return DoNotDisturbSettings::whenScreensMirrored(); +} + +void Settings::setInhibitNotificationsWhenScreensMirrored(bool inhibit) +{ + if (inhibit == inhibitNotificationsWhenScreensMirrored()) { + return; + } + + DoNotDisturbSettings::setWhenScreensMirrored(inhibit); + d->setDirty(true); +} + +bool Settings::screensMirrored() const +{ + return d->mirroredScreensTracker && d->mirroredScreensTracker->screensMirrored(); +} + +void Settings::setScreensMirrored(bool mirrored) +{ + if (mirrored) { + qCWarning(NOTIFICATIONMANAGER) << "Cannot forcefully set screens mirrored"; + return; + } + + if (d->mirroredScreensTracker) { + d->mirroredScreensTracker->setScreensMirrored(mirrored); + } +} + void Settings::revokeApplicationInhibitions() { Server::self().clearInhibitions(); } bool Settings::notificationSoundsInhibited() const { return DoNotDisturbSettings::notificationSoundsMuted(); } void Settings::setNotificationSoundsInhibited(bool inhibited) { if (inhibited == notificationSoundsInhibited()) { return; } DoNotDisturbSettings::setNotificationSoundsMuted(inhibited); d->setDirty(true); } diff --git a/libnotificationmanager/settings.h b/libnotificationmanager/settings.h index 5e46e6f3e..30b617d83 100644 --- a/libnotificationmanager/settings.h +++ b/libnotificationmanager/settings.h @@ -1,332 +1,362 @@ /* * Copyright 2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include #include #include #include #include #include "notificationmanager_export.h" namespace NotificationManager { /** * @short Notification settings and state * * This class encapsulates all global settings related to notifications * as well as do not disturb mode and other state. * * This class can be used by applications to alter their behavior * depending on user's notification preferences. * * @author Kai Uwe Broulik **/ class NOTIFICATIONMANAGER_EXPORT Settings : public QObject { Q_OBJECT /** * Whether to show critical notification popups in do not disturb mode. */ Q_PROPERTY(bool criticalPopupsInDoNotDisturbMode READ criticalPopupsInDoNotDisturbMode WRITE setCriticalPopupsInDoNotDisturbMode NOTIFY settingsChanged) /** * Whether to keep critical notifications always on top. */ Q_PROPERTY(bool keepCriticalAlwaysOnTop READ keepCriticalAlwaysOnTop WRITE setKeepCriticalAlwaysOnTop NOTIFY settingsChanged) /** * Whether to show popups for low priority notifications. */ Q_PROPERTY(bool lowPriorityPopups READ lowPriorityPopups WRITE setLowPriorityPopups NOTIFY settingsChanged) /** * Whether to add low priority notifications to the history. */ Q_PROPERTY(bool lowPriorityHistory READ lowPriorityHistory WRITE setLowPriorityHistory NOTIFY settingsChanged) /** * The notification popup position on screen. * CloseToWidget means they should be positioned closely to where the plasmoid is located on screen. */ Q_PROPERTY(PopupPosition popupPosition READ popupPosition WRITE setPopupPosition NOTIFY settingsChanged) /** * The default timeout for notification popups that do not have an explicit timeout set, * in milliseconds. Default is 5000ms (5 seconds). */ Q_PROPERTY(int popupTimeout READ popupTimeout WRITE setPopupTimeout RESET resetPopupTimeout NOTIFY settingsChanged) /** * Whether to show application jobs in task manager */ Q_PROPERTY(bool jobsInTaskManager READ jobsInTaskManager WRITE setJobsInTaskManager /*RESET resetJobsInTaskManager*/ NOTIFY settingsChanged) /** * Whether to show application jobs as notifications */ Q_PROPERTY(bool jobsInNotifications READ jobsInNotifications WRITE setJobsInNotifications /*RESET resetJobsPopup*/ NOTIFY settingsChanged) /** * Whether application jobs stay visible for the whole duration of the job */ Q_PROPERTY(bool permanentJobPopups READ permanentJobPopups WRITE setPermanentJobPopups /*RESET resetAutoHideJobsPopup*/ NOTIFY settingsChanged) /** * Whether to show notification badges (numbers in circles) in task manager */ Q_PROPERTY(bool badgesInTaskManager READ badgesInTaskManager WRITE setBadgesInTaskManager NOTIFY settingsChanged) /** * A list of desktop entries of applications that have been seen sending a notification. */ Q_PROPERTY(QStringList knownApplications READ knownApplications NOTIFY knownApplicationsChanged) /** * A list of desktop entries of applications for which no popups should be shown. */ Q_PROPERTY(QStringList popupBlacklistedApplications READ popupBlacklistedApplications NOTIFY settingsChanged) /** * A list of notifyrc names of services for which no popups should be shown. */ Q_PROPERTY(QStringList popupBlacklistedServices READ popupBlacklistedServices NOTIFY settingsChanged) /** * A list of desktop entries of applications for which a popup should be shown even in do not disturb mode. */ Q_PROPERTY(QStringList doNotDisturbPopupWhitelistedApplications READ doNotDisturbPopupWhitelistedApplications NOTIFY settingsChanged) /** * A list of notifyrc names of services for which a popup should be shown even in do not disturb mode. */ Q_PROPERTY(QStringList doNotDisturbPopupWhitelistedServices READ doNotDisturbPopupWhitelistedServices NOTIFY settingsChanged) /** * A list of desktop entries of applications which shouldn't be shown in the history. */ Q_PROPERTY(QStringList historyBlacklistedApplications READ historyBlacklistedApplications NOTIFY settingsChanged) /** * A list of notifyrc names of services which shouldn't be shown in the history. */ Q_PROPERTY(QStringList historyBlacklistedServices READ historyBlacklistedServices NOTIFY settingsChanged) /** * A list of desktop entries of applications which shouldn't show badges in task manager. */ Q_PROPERTY(QStringList badgeBlacklistedApplications READ badgeBlacklistedApplications NOTIFY settingsChanged) /** * The date until which do not disturb mode is enabled. * * When invalid or in the past, do not disturb mode should be considered disabled. * Do not disturb mode is considered active when this property points to a date * in the future OR notificationsInhibitedByApplication is true. */ Q_PROPERTY(QDateTime notificationsInhibitedUntil READ notificationsInhibitedUntil WRITE setNotificationsInhibitedUntil RESET resetNotificationsInhibitedUntil NOTIFY settingsChanged) /** * Whether an application currently requested do not disturb mode. * * Do not disturb mode is considered active when this property is true OR * notificationsInhibitedUntil points to a date in the future. * * @sa revokeApplicationInhibitions */ Q_PROPERTY(bool notificationsInhibitedByApplication READ notificationsInhibitedByApplication NOTIFY notificationsInhibitedByApplicationChanged) Q_PROPERTY(QStringList notificationInhibitionApplications READ notificationInhibitionApplications NOTIFY notificationInhibitionApplicationsChanged) Q_PROPERTY(QStringList notificationInhibitionReasons READ notificationInhibitionReasons NOTIFY notificationInhibitionApplicationsChanged) + /** + * Whether to enable do not disturb mode when screens are mirrored/overlapping + * + * @since 5.17 + */ + Q_PROPERTY(bool inhibitNotificationsWhenScreensMirrored + READ inhibitNotificationsWhenScreensMirrored + WRITE setInhibitNotificationsWhenScreensMirrored + NOTIFY settingsChanged) + + /** + * Whether there currently are mirrored/overlapping screens + * + * This property is only updated when @c inhibitNotificationsWhenScreensMirrored + * is set to true, otherwise it is always false. + * You can assign false to this property if you want to temporarily revoke automatic do not disturb + * mode when screens are mirrored until the screen configuration changes. + * + * @since 5.17 + */ + Q_PROPERTY(bool screensMirrored READ screensMirrored WRITE setScreensMirrored NOTIFY screensMirroredChanged) + /** * Whether notification sounds should be disabled * * This does not reflect the actual mute state of the Notification Sounds * stream but only remembers what value was assigned to this property. * * This way you can tell whether to unmute notification sounds or not, in case * the user had them explicitly muted previously. * * @note This does not actually mute or unmute the actual sound stream, * you need to do this yourself using e.g. PulseAudio. */ Q_PROPERTY(bool notificationSoundsInhibited READ notificationSoundsInhibited WRITE setNotificationSoundsInhibited NOTIFY settingsChanged) /** * Whether to update the properties immediately when they are changed on disk * * This can be undesirable for a settings dialog where outside changes * should not suddenly cause the UI to change. * * Default is true. */ Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged) /** * Whether the settings have changed and need to be saved * * @sa save() */ Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged) public: explicit Settings(QObject *parent = nullptr); Settings(const KSharedConfig::Ptr &config, QObject *parent = nullptr); ~Settings() override; enum PopupPosition { CloseToWidget = 0, TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight }; Q_ENUM(PopupPosition) enum NotificationBehavior { ShowPopups = 1 << 1, ShowPopupsInDoNotDisturbMode = 1 << 2, ShowInHistory = 1 << 3, ShowBadges = 1 << 4 }; Q_ENUM(NotificationBehavior) Q_DECLARE_FLAGS(NotificationBehaviors, NotificationBehavior) Q_FLAG(NotificationBehaviors) Q_INVOKABLE NotificationBehaviors applicationBehavior(const QString &desktopEntry) const; Q_INVOKABLE void setApplicationBehavior(const QString &desktopEntry, NotificationBehaviors behaviors); Q_INVOKABLE NotificationBehaviors serviceBehavior(const QString &desktopEntry) const; Q_INVOKABLE void setServiceBehavior(const QString &desktopEntry, NotificationBehaviors behaviors); Q_INVOKABLE void registerKnownApplication(const QString &desktopEntry); Q_INVOKABLE void forgetKnownApplication(const QString &desktopEntry); Q_INVOKABLE void load(); Q_INVOKABLE void save(); Q_INVOKABLE void defaults(); bool live() const; void setLive(bool live); bool dirty() const; bool criticalPopupsInDoNotDisturbMode() const; void setCriticalPopupsInDoNotDisturbMode(bool enable); bool keepCriticalAlwaysOnTop() const; void setKeepCriticalAlwaysOnTop(bool enable); bool lowPriorityPopups() const; void setLowPriorityPopups(bool enable); bool lowPriorityHistory() const; void setLowPriorityHistory(bool enable); PopupPosition popupPosition() const; void setPopupPosition(PopupPosition popupPosition); int popupTimeout() const; void setPopupTimeout(int popupTimeout); void resetPopupTimeout(); bool jobsInTaskManager() const; void setJobsInTaskManager(bool enable); bool jobsInNotifications() const; void setJobsInNotifications(bool enable); bool permanentJobPopups() const; void setPermanentJobPopups(bool enable); bool badgesInTaskManager() const; void setBadgesInTaskManager(bool enable); QStringList knownApplications() const; QStringList popupBlacklistedApplications() const; QStringList popupBlacklistedServices() const; QStringList doNotDisturbPopupWhitelistedApplications() const; QStringList doNotDisturbPopupWhitelistedServices() const; QStringList historyBlacklistedApplications() const; QStringList historyBlacklistedServices() const; QStringList badgeBlacklistedApplications() const; QDateTime notificationsInhibitedUntil() const; void setNotificationsInhibitedUntil(const QDateTime &time); void resetNotificationsInhibitedUntil(); bool notificationsInhibitedByApplication() const; QStringList notificationInhibitionApplications() const; QStringList notificationInhibitionReasons() const; + bool inhibitNotificationsWhenScreensMirrored() const; + void setInhibitNotificationsWhenScreensMirrored(bool mirrored); + + bool screensMirrored() const; + void setScreensMirrored(bool enable); + bool notificationSoundsInhibited() const; void setNotificationSoundsInhibited(bool inhibited); /** * Revoke application notification inhibitions. * * @note Applications are not notified of the fact that their * inhibition might have been taken away. */ Q_INVOKABLE void revokeApplicationInhibitions(); signals: void settingsChanged(); void liveChanged(); void dirtyChanged(); void knownApplicationsChanged(); void notificationsInhibitedByApplicationChanged(bool notificationsInhibitedByApplication); void notificationInhibitionApplicationsChanged(); + void screensMirroredChanged(); + private: class Private; QScopedPointer d; }; } // namespace NotificationManager Q_DECLARE_OPERATORS_FOR_FLAGS(NotificationManager::Settings::NotificationBehaviors)