diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index aa5b2d000..5a97467c4 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -36,6 +36,9 @@ Singleton { property bool isLightMode: false property bool doNotDisturb: false property real doNotDisturbUntil: 0 + property bool doNotDisturbHeldByScreenShare: false + property bool screenShareDndDismissed: false + property var screenShareDismissedIds: [] property bool idleInhibited: false property real idleInhibitedUntil: 0 property string terminalOverride: "" @@ -72,7 +75,15 @@ Singleton { id: dndExpireTimer repeat: false running: false - onTriggered: root.setDoNotDisturb(false) + onTriggered: root.setDoNotDisturb(false, 0, "timer") + } + + Timer { + id: screenShareReleaseTimer + interval: 4000 + repeat: false + running: false + onTriggered: root._endScreenShareDnd() } function _armDndExpireTimer() { @@ -81,7 +92,7 @@ Singleton { return; const remaining = doNotDisturbUntil - Date.now(); if (remaining <= 0) { - setDoNotDisturb(false); + setDoNotDisturb(false, 0, "timer"); return; } dndExpireTimer.interval = remaining; @@ -109,6 +120,21 @@ Singleton { root.suppressOSD = true; osdSuppressTimer.restart(); root._applyDndExpirySanity(); + Qt.callLater(root._syncScreenShareDnd); + } + } + + Connections { + target: (!root.isGreeterMode && typeof SettingsData !== "undefined" && SettingsData.notificationDndWhileScreenSharing && typeof PrivacyService !== "undefined") ? PrivacyService : null + function onScreensharingActiveChanged() { + root._syncScreenShareDnd(); + } + } + + Connections { + target: typeof SettingsData !== "undefined" ? SettingsData : null + function onNotificationDndWhileScreenSharingChanged() { + root._syncScreenShareDnd(); } } @@ -311,6 +337,7 @@ Singleton { Theme.generateSystemThemesFromCurrentTheme(); loaded(); + Qt.callLater(root._syncScreenShareDnd); _checkSessionWritable(); } catch (e) { @@ -393,6 +420,7 @@ Singleton { Theme.generateSystemThemesFromCurrentTheme(); loaded(); + Qt.callLater(root._syncScreenShareDnd); } catch (e) { _parseError = true; const msg = e.message; @@ -645,11 +673,24 @@ Singleton { }); } - function setDoNotDisturb(enabled, durationMinutes) { + function setDoNotDisturb(enabled, durationMinutes, origin) { + const src = origin || "user"; + if (src === "user") { + if (!enabled && _screenShareShouldHold()) { + screenShareDndDismissed = true; + screenShareDismissedIds = _screencastIds(); + } + if (doNotDisturbHeldByScreenShare) + doNotDisturbHeldByScreenShare = false; + } + const minutes = Number(durationMinutes) || 0; doNotDisturb = enabled; doNotDisturbUntil = (enabled && minutes > 0) ? Date.now() + minutes * 60 * 1000 : 0; saveSettings(); + + if (src !== "screenshare") + Qt.callLater(_syncScreenShareDnd); } function setIdleInhibited(enabled, durationMinutes) { @@ -669,11 +710,92 @@ Singleton { setDoNotDisturb(false); return; } + if (doNotDisturbHeldByScreenShare) + doNotDisturbHeldByScreenShare = false; doNotDisturb = true; doNotDisturbUntil = target; saveSettings(); } + function _screenShareShouldHold() { + if (isGreeterMode) + return false; + if (typeof SettingsData === "undefined" || !SettingsData.notificationDndWhileScreenSharing) + return false; + if (typeof PrivacyService === "undefined" || !PrivacyService.screensharingActive) + return false; + return true; + } + + function _screencastIds() { + return (typeof PrivacyService !== "undefined") ? PrivacyService.screencastSourceIds() : []; + } + + function _dismissalApplies() { + if (!screenShareDndDismissed) + return false; + const dismissed = screenShareDismissedIds || []; + if (!dismissed.length) + return true; + const current = _screencastIds(); + if (!current.length) + return true; + return current.some(id => dismissed.indexOf(id) !== -1); + } + + function _releaseScreenShareDndHold() { + if (!doNotDisturbHeldByScreenShare) + return; + doNotDisturbHeldByScreenShare = false; + if (!doNotDisturb || doNotDisturbUntil > 0) { + saveSettings(); + return; + } + setDoNotDisturb(false, 0, "screenshare"); + } + + function _endScreenShareDnd() { + _clearScreenShareDismissal(); + _releaseScreenShareDndHold(); + } + + function _clearScreenShareDismissal() { + if (!screenShareDndDismissed) + return; + screenShareDndDismissed = false; + screenShareDismissedIds = []; + saveSettings(); + } + + function _syncScreenShareDnd() { + if (isGreeterMode) + return; + + if (!_screenShareShouldHold()) { + if (!doNotDisturbHeldByScreenShare && !screenShareDndDismissed) + return; + const settingOn = typeof SettingsData !== "undefined" && SettingsData.notificationDndWhileScreenSharing; + if (!settingOn) { + screenShareReleaseTimer.stop(); + _endScreenShareDnd(); + return; + } + screenShareReleaseTimer.restart(); + return; + } + + screenShareReleaseTimer.stop(); + + if (!_dismissalApplies()) + _clearScreenShareDismissal(); + + if (screenShareDndDismissed || doNotDisturb) + return; + + doNotDisturbHeldByScreenShare = true; + setDoNotDisturb(true, 0, "screenshare"); + } + function setWallpaper(imagePath) { wallpaperPath = imagePath; if (perModeWallpaper) { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 0e2988031..b0f1d3349 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -988,6 +988,7 @@ Singleton { property bool notificationHistorySaveCritical: true property var notificationRules: [] property bool notificationDndAllowCritical: true + property bool notificationDndWhileScreenSharing: true property bool notificationFocusedMonitor: false // Island is a per-instance render mode: any bar config with island:true draws as an island // instead of a DankBar, and carries its own island* look settings. diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index be5c27912..cc1324808 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -4,6 +4,9 @@ var SPEC = { isLightMode: { def: false }, doNotDisturb: { def: false }, doNotDisturbUntil: { def: 0 }, + doNotDisturbHeldByScreenShare: { def: false }, + screenShareDndDismissed: { def: false }, + screenShareDismissedIds: { def: [] }, idleInhibited: { def: false }, idleInhibitedUntil: { def: 0 }, terminalOverride: { def: "" }, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index cb89eafb2..27453b4e4 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -533,6 +533,7 @@ var SPEC = { notificationHistorySaveCritical: { def: true }, notificationRules: { def: [] }, notificationDndAllowCritical: { def: true }, + notificationDndWhileScreenSharing: { def: true }, notificationFocusedMonitor: { def: false }, diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index f6322d9fb..b996ba425 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -429,6 +429,15 @@ Item { onToggled: checked => SessionData.setDoNotDisturb(checked) } + SettingsToggleRow { + settingKey: "notificationDndWhileScreenSharing" + tags: ["notification", "dnd", "screenshare", "sharing", "privacy"] + text: I18n.tr("Automatically enable while screen sharing", "do not disturb auto enable while screen sharing toggle") + description: I18n.tr("Turn on Do Not Disturb while a screen share is active", "do not disturb auto enable while screen sharing description") + checked: SettingsData.notificationDndWhileScreenSharing + onToggled: checked => SettingsData.set("notificationDndWhileScreenSharing", checked) + } + SettingsToggleRow { settingKey: "notificationDndAllowCritical" tags: ["notification", "dnd", "critical", "priority", "urgent", "bypass"] diff --git a/quickshell/Services/PrivacyService.qml b/quickshell/Services/PrivacyService.qml index 49088621d..1c0c12f36 100644 --- a/quickshell/Services/PrivacyService.qml +++ b/quickshell/Services/PrivacyService.qml @@ -126,6 +126,37 @@ Singleton { return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined) } + function screencastSourceIds() { + const ids = [] + + if (CompositorService.isNiri) { + for (const cast of NiriService.casts) { + if (cast && cast.is_active) { + ids.push("niri:" + cast.stream_id) + } + } + } + + if (!Pipewire.ready || !Pipewire.nodes?.values) { + return ids + } + + for (let i = 0; i < Pipewire.nodes.values.length; i++) { + const node = Pipewire.nodes.values[i] + if (!node || !node.ready) { + continue + } + + const isVideoSource = (node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource + const isVideoStream = node.properties && node.properties["media.class"] === "Stream/Output/Video" + if ((isVideoSource || isVideoStream) && looksLikeScreencast(node)) { + ids.push("pw:" + node.id) + } + } + + return ids.sort() + } + function getMicrophoneStatus() { return microphoneActive ? "active" : "inactive" } diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 39616545b..4e3b4fed4 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -7128,6 +7128,32 @@ ], "description": "Control animation duration for notification popups and history" }, + { + "section": "notificationDndWhileScreenSharing", + "label": "Automatically enable while screen sharing", + "tabIndex": 17, + "category": "Notifications", + "keywords": [ + "active", + "alerts", + "automatically", + "disturb", + "dnd", + "enable", + "messages", + "notification", + "notifications", + "privacy", + "screen", + "screenshare", + "share", + "sharing", + "toast", + "turn", + "while" + ], + "description": "Turn on Do Not Disturb while a screen share is active" + }, { "section": "notificationPopupBodyInvokesAction", "label": "Body Click Runs Default Action",