diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index e3a07f5a..903a53a7 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -29,9 +29,33 @@ Singleton { property bool isLightMode: false property bool doNotDisturb: false + property real doNotDisturbUntil: 0 property bool isSwitchingMode: false property bool suppressOSD: true + Timer { + id: dndExpireTimer + repeat: false + running: false + onTriggered: root.setDoNotDisturb(false) + } + + function _armDndExpireTimer() { + dndExpireTimer.stop(); + if (!doNotDisturb || doNotDisturbUntil <= 0) + return; + const remaining = doNotDisturbUntil - Date.now(); + if (remaining <= 0) { + setDoNotDisturb(false); + return; + } + dndExpireTimer.interval = remaining; + dndExpireTimer.start(); + } + + onDoNotDisturbChanged: _armDndExpireTimer() + onDoNotDisturbUntilChanged: _armDndExpireTimer() + Timer { id: osdSuppressTimer interval: 2000 @@ -49,6 +73,7 @@ Singleton { function onSessionResumed() { root.suppressOSD = true; osdSuppressTimer.restart(); + root._applyDndExpirySanity(); } } @@ -190,6 +215,7 @@ Singleton { } Store.parse(root, obj); + _applyDndExpirySanity(); _loadedSessionSnapshot = getCurrentSessionJson(); _hasLoaded = true; @@ -271,6 +297,7 @@ Singleton { } Store.parse(root, obj); + _applyDndExpirySanity(); _loadedSessionSnapshot = getCurrentSessionJson(); _hasLoaded = true; @@ -288,6 +315,16 @@ Singleton { } } + function _applyDndExpirySanity() { + if (doNotDisturb && doNotDisturbUntil > 0 && Date.now() >= doNotDisturbUntil) { + doNotDisturb = false; + doNotDisturbUntil = 0; + } else if (!doNotDisturb && doNotDisturbUntil !== 0) { + doNotDisturbUntil = 0; + } + _armDndExpireTimer(); + } + function saveSettings() { if (isGreeterMode || _parseError || !_hasLoaded) return; @@ -357,8 +394,21 @@ Singleton { }); } - function setDoNotDisturb(enabled) { + function setDoNotDisturb(enabled, durationMinutes) { + const minutes = Number(durationMinutes) || 0; doNotDisturb = enabled; + doNotDisturbUntil = (enabled && minutes > 0) ? Date.now() + minutes * 60 * 1000 : 0; + saveSettings(); + } + + function setDoNotDisturbUntilTimestamp(timestampMs) { + const target = Number(timestampMs) || 0; + if (target <= Date.now()) { + setDoNotDisturb(false); + return; + } + doNotDisturb = true; + doNotDisturbUntil = target; saveSettings(); } diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index e2bb4fe8..f9d465de 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -3,6 +3,7 @@ var SPEC = { isLightMode: { def: false }, doNotDisturb: { def: false }, + doNotDisturbUntil: { def: 0 }, wallpaperPath: { def: "" }, perMonitorWallpaper: { def: false }, diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml index 3c555904..2d7eef91 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -372,10 +372,10 @@ Popup { anchors.fill: parent implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + color: BlurService.enabled ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) radius: Theme.cornerRadius - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 Rectangle { anchors.fill: parent @@ -438,7 +438,7 @@ Popup { if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) { return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); } - return itemMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"; + return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; } Row { diff --git a/quickshell/Modals/NotificationModal.qml b/quickshell/Modals/NotificationModal.qml index 86533444..f0e5e1a2 100644 --- a/quickshell/Modals/NotificationModal.qml +++ b/quickshell/Modals/NotificationModal.qml @@ -144,10 +144,48 @@ DankModal { return "NOTIFICATION_MODAL_TOGGLE_DND_SUCCESS"; } + function enableDoNotDisturbFor(minutes: int): string { + if (minutes <= 0) { + return "ERROR: minutes must be > 0"; + } + SessionData.setDoNotDisturb(true, minutes); + return "NOTIFICATION_MODAL_DND_SET_FOR_" + minutes + "_SUCCESS"; + } + + function enableDoNotDisturbUntil(timestampMs: string): string { + const ts = Number(timestampMs); + if (!ts || ts <= Date.now()) { + return "ERROR: timestamp must be a future epoch ms"; + } + SessionData.setDoNotDisturbUntilTimestamp(ts); + return "NOTIFICATION_MODAL_DND_SET_UNTIL_SUCCESS"; + } + + function enableDoNotDisturbIndefinitely(): string { + SessionData.setDoNotDisturb(true, 0); + return "NOTIFICATION_MODAL_DND_INDEFINITE_SUCCESS"; + } + + function enableDoNotDisturbUntilTomorrowMorning(): string { + const now = new Date(); + const target = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8, 0, 0, 0); + SessionData.setDoNotDisturbUntilTimestamp(target.getTime()); + return "NOTIFICATION_MODAL_DND_UNTIL_TOMORROW_SUCCESS"; + } + + function disableDoNotDisturb(): string { + SessionData.setDoNotDisturb(false); + return "NOTIFICATION_MODAL_DND_DISABLE_SUCCESS"; + } + function getDoNotDisturb(): bool { return SessionData.doNotDisturb; } + function getDoNotDisturbUntil(): string { + return String(SessionData.doNotDisturbUntil); + } + function clearAll(): string { notificationModal.clearAll(); return "NOTIFICATION_MODAL_CLEAR_ALL_SUCCESS"; diff --git a/quickshell/Modules/ControlCenter/Components/DetailHost.qml b/quickshell/Modules/ControlCenter/Components/DetailHost.qml index a3b11564..b444fa06 100644 --- a/quickshell/Modules/ControlCenter/Components/DetailHost.qml +++ b/quickshell/Modules/ControlCenter/Components/DetailHost.qml @@ -188,6 +188,9 @@ Item { case "battery": coreDetailLoader.sourceComponent = batteryDetailComponent; break; + case "doNotDisturb": + coreDetailLoader.sourceComponent = doNotDisturbDetailComponent; + break; default: return; } @@ -230,6 +233,11 @@ Item { BatteryDetail {} } + Component { + id: doNotDisturbDetailComponent + DoNotDisturbDetail {} + } + Component { id: diskUsageDetailComponent DiskUsageDetail { diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index ff70ed6f..c55e4b0c 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -163,6 +163,8 @@ Column { return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent; } else if (id === "colorPicker") { return colorPickerPillComponent; + } else if (id === "doNotDisturb") { + return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent; } else { return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent; } @@ -573,6 +575,22 @@ Column { } } + Component { + id: dndPillComponent + DndPill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 60 + + onExpandClicked: { + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex); + } + } + } + } + Component { id: smallBatteryComponent SmallBatteryButton { @@ -603,8 +621,6 @@ Column { return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode"; case "darkMode": return "contrast"; - case "doNotDisturb": - return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"; case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"; default: @@ -618,8 +634,6 @@ Column { return I18n.tr("Night Mode"); case "darkMode": return I18n.tr("Dark Mode"); - case "doNotDisturb": - return I18n.tr("Do Not Disturb"); case "idleInhibitor": return SessionService.idleInhibited ? I18n.tr("Keeping Awake") : I18n.tr("Keep Awake"); default: @@ -642,8 +656,6 @@ Column { return DisplayService.nightModeEnabled || false; case "darkMode": return !SessionData.isLightMode; - case "doNotDisturb": - return SessionData.doNotDisturb || false; case "idleInhibitor": return SessionService.idleInhibited || false; default: @@ -670,11 +682,6 @@ Column { Theme.setLightMode(newMode); break; } - case "doNotDisturb": - { - SessionData.setDoNotDisturb(!SessionData.doNotDisturb); - break; - } case "idleInhibitor": { SessionService.toggleIdleInhibit(); diff --git a/quickshell/Modules/ControlCenter/Details/DoNotDisturbDetail.qml b/quickshell/Modules/ControlCenter/Details/DoNotDisturbDetail.qml new file mode 100644 index 00000000..c3c17417 --- /dev/null +++ b/quickshell/Modules/ControlCenter/Details/DoNotDisturbDetail.qml @@ -0,0 +1,258 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 0 + + property real nowMs: Date.now() + + Timer { + interval: 1000 + repeat: true + running: root.visible && SessionData.doNotDisturb && SessionData.doNotDisturbUntil > 0 + onTriggered: root.nowMs = Date.now() + } + + function _pad2(n) { + return n < 10 ? "0" + n : "" + n; + } + + function formatUntil(ts) { + if (!ts) + return ""; + const d = new Date(ts); + const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true; + if (use24h) + return _pad2(d.getHours()) + ":" + _pad2(d.getMinutes()); + const suffix = d.getHours() >= 12 ? "PM" : "AM"; + const h12 = ((d.getHours() + 11) % 12) + 1; + return h12 + ":" + _pad2(d.getMinutes()) + " " + suffix; + } + + function formatRemaining(ms) { + if (ms <= 0) + return ""; + const totalMinutes = Math.ceil(ms / 60000); + if (totalMinutes < 60) + return I18n.tr("%1 min left").arg(totalMinutes); + const hours = Math.floor(totalMinutes / 60); + const mins = totalMinutes - hours * 60; + if (mins === 0) + return I18n.tr("%1 h left").arg(hours); + return I18n.tr("%1 h %2 m left").arg(hours).arg(mins); + } + + function minutesUntilTomorrowMorning() { + const now = new Date(); + const target = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8, 0, 0, 0); + return Math.max(1, Math.round((target.getTime() - now.getTime()) / 60000)); + } + + readonly property var presets: [ + { + "label": I18n.tr("15 min"), + "minutes": 15 + }, + { + "label": I18n.tr("30 min"), + "minutes": 30 + }, + { + "label": I18n.tr("1 hour"), + "minutes": 60 + }, + { + "label": I18n.tr("3 hours"), + "minutes": 180 + }, + { + "label": I18n.tr("8 hours"), + "minutes": 480 + }, + { + "label": I18n.tr("Until 8 AM"), + "minutesFn": true + } + ] + + Column { + id: contentColumn + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: SessionData.doNotDisturb ? "do_not_disturb_on" : "notifications_paused" + size: Theme.iconSizeLarge + color: SessionData.doNotDisturb ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSizeLarge - Theme.spacingM + spacing: 2 + + StyledText { + text: I18n.tr("Silence notifications") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: { + if (!SessionData.doNotDisturb) + return I18n.tr("Pick how long to pause notifications"); + if (SessionData.doNotDisturbUntil <= 0) + return I18n.tr("On indefinitely"); + const remaining = Math.max(0, SessionData.doNotDisturbUntil - root.nowMs); + return root.formatRemaining(remaining) + " · " + I18n.tr("until %1").arg(root.formatUntil(SessionData.doNotDisturbUntil)); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + elide: Text.ElideRight + } + } + } + + Grid { + width: parent.width + columns: 3 + columnSpacing: Theme.spacingS + rowSpacing: Theme.spacingS + + Repeater { + model: root.presets + + Rectangle { + required property var modelData + width: (contentColumn.width - Theme.spacingS * 2) / 3 + height: 36 + radius: Theme.cornerRadius + color: presetArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + + MouseArea { + id: presetArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + const minutes = modelData.minutesFn ? root.minutesUntilTomorrowMorning() : modelData.minutes; + SessionData.setDoNotDisturb(true, minutes); + } + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + width: (contentColumn.width - Theme.spacingS) / 2 + height: 36 + radius: Theme.cornerRadius + color: foreverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "block" + size: Theme.iconSizeSmall + color: Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Until I turn it off") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + MouseArea { + id: foreverArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SessionData.setDoNotDisturb(true, 0) + } + } + + Rectangle { + width: (contentColumn.width - Theme.spacingS) / 2 + height: 36 + radius: Theme.cornerRadius + visible: SessionData.doNotDisturb + color: offArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.18) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "notifications_active" + size: Theme.iconSizeSmall + color: offArea.containsMouse ? Theme.error : Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Turn off") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: offArea.containsMouse ? Theme.error : Theme.surfaceText + } + } + + MouseArea { + id: offArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SessionData.setDoNotDisturb(false) + } + } + } + } +} diff --git a/quickshell/Modules/ControlCenter/Widgets/DndPill.qml b/quickshell/Modules/ControlCenter/Widgets/DndPill.qml new file mode 100644 index 00000000..24507e97 --- /dev/null +++ b/quickshell/Modules/ControlCenter/Widgets/DndPill.qml @@ -0,0 +1,29 @@ +import QtQuick +import qs.Common +import qs.Modules.ControlCenter.Widgets + +CompoundPill { + id: root + + iconName: SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" + iconColor: SessionData.doNotDisturb ? Theme.primary : Theme.surfaceText + primaryText: I18n.tr("Do Not Disturb") + isActive: SessionData.doNotDisturb + + secondaryText: { + if (!SessionData.doNotDisturb) + return I18n.tr("Off"); + if (SessionData.doNotDisturbUntil <= 0) + return I18n.tr("On"); + const d = new Date(SessionData.doNotDisturbUntil); + const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true; + const pad = n => n < 10 ? "0" + n : "" + n; + if (use24h) + return I18n.tr("Until %1").arg(pad(d.getHours()) + ":" + pad(d.getMinutes())); + const suffix = d.getHours() >= 12 ? "PM" : "AM"; + const h12 = ((d.getHours() + 11) % 12) + 1; + return I18n.tr("Until %1").arg(h12 + ":" + pad(d.getMinutes()) + " " + suffix); + } + + onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) +} diff --git a/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml b/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml index 46c4f693..eb73da3b 100644 --- a/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml +++ b/quickshell/Modules/DankBar/Widgets/AppsDockContextMenu.qml @@ -9,6 +9,15 @@ import qs.Widgets PanelWindow { id: root + WindowBlur { + targetWindow: root + blurX: menuContainer.x + blurY: menuContainer.y + blurWidth: root.visible ? menuContainer.width : 0 + blurHeight: root.visible ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + WlrLayershell.namespace: "dms:dock-context-menu" property var appData: null @@ -112,8 +121,8 @@ PanelWindow { height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) radius: Theme.cornerRadius - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 opacity: root.visible ? 1 : 0 visible: opacity > 0 @@ -165,7 +174,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: windowArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" StyledText { anchors.left: parent.left @@ -255,7 +264,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: actionArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" Row { anchors.left: parent.left @@ -330,7 +339,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: pinArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" StyledText { anchors.left: parent.left @@ -390,7 +399,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: nvidiaArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" StyledText { anchors.left: parent.left diff --git a/quickshell/Modules/DankBar/Widgets/NotepadButton.qml b/quickshell/Modules/DankBar/Widgets/NotepadButton.qml index 08f5e6bc..77a5e7b7 100644 --- a/quickshell/Modules/DankBar/Widgets/NotepadButton.qml +++ b/quickshell/Modules/DankBar/Widgets/NotepadButton.qml @@ -148,6 +148,15 @@ BasePill { PanelWindow { id: contextMenuWindow + WindowBlur { + targetWindow: contextMenuWindow + blurX: menuContainer.x + blurY: menuContainer.y + blurWidth: contextMenuWindow.visible ? menuContainer.width : 0 + blurHeight: contextMenuWindow.visible ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + WlrLayershell.namespace: "dms:notepad-context-menu" property bool isVertical: false @@ -244,8 +253,8 @@ BasePill { height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) radius: Theme.cornerRadius - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 opacity: contextMenuWindow.visible ? 1 : 0 visible: opacity > 0 diff --git a/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml b/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml index d7bcc1e9..e8649071 100644 --- a/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml +++ b/quickshell/Modules/DankBar/Widgets/NotificationCenterButton.qml @@ -1,5 +1,6 @@ import QtQuick import qs.Common +import qs.Modules.Notifications.Center import qs.Modules.Plugins import qs.Widgets @@ -34,7 +35,49 @@ BasePill { } } - onRightClicked: { - SessionData.setDoNotDisturb(!SessionData.doNotDisturb); + onRightClicked: (rx, ry) => { + const screen = root.parentScreen || Screen; + if (!screen) + return; + const globalPos = root.visualContent.mapToItem(null, 0, 0); + const isVertical = root.axis?.isVertical ?? false; + const edge = root.axis?.edge ?? "top"; + const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS); + const barOffset = root.barThickness + root.barSpacing + gap; + + let anchorX; + let anchorY; + let anchorEdge; + if (isVertical) { + anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2; + if (edge === "left") { + anchorX = barOffset; + anchorEdge = "top"; + } else { + anchorX = screen.width - barOffset; + anchorEdge = "top"; + } + } else { + anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2; + if (edge === "bottom") { + anchorY = screen.height - barOffset; + anchorEdge = "bottom"; + } else { + anchorY = barOffset; + anchorEdge = "top"; + } + } + + dndPopupLoader.active = true; + const popup = dndPopupLoader.item; + if (!popup) + return; + popup.showAt(anchorX, anchorY, screen, anchorEdge); + } + + Loader { + id: dndPopupLoader + active: false + sourceComponent: DndDurationPopup {} } } diff --git a/quickshell/Modules/Dock/DockContextMenu.qml b/quickshell/Modules/Dock/DockContextMenu.qml index 183f3317..61b4bbc9 100644 --- a/quickshell/Modules/Dock/DockContextMenu.qml +++ b/quickshell/Modules/Dock/DockContextMenu.qml @@ -230,7 +230,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: windowArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" StyledText { anchors.left: parent.left @@ -320,7 +320,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: actionArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" Row { anchors.left: parent.left @@ -395,7 +395,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: pinArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" Row { anchors.left: parent.left @@ -468,7 +468,7 @@ PanelWindow { width: parent.width height: 28 radius: Theme.cornerRadius - color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + color: nvidiaArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" Row { anchors.left: parent.left diff --git a/quickshell/Modules/Notifications/Center/DndDurationMenu.qml b/quickshell/Modules/Notifications/Center/DndDurationMenu.qml new file mode 100644 index 00000000..f3e96ec9 --- /dev/null +++ b/quickshell/Modules/Notifications/Center/DndDurationMenu.qml @@ -0,0 +1,250 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + signal dismissed + + readonly property bool currentlyActive: SessionData.doNotDisturb + readonly property real currentRemainingMs: SessionData.doNotDisturbUntil > 0 ? Math.max(0, SessionData.doNotDisturbUntil - nowMs) : 0 + property real nowMs: Date.now() + + Timer { + interval: 1000 + repeat: true + running: root.visible && root.currentlyActive && SessionData.doNotDisturbUntil > 0 + onTriggered: root.nowMs = Date.now() + } + + function _pad2(n) { + return n < 10 ? "0" + n : "" + n; + } + + function formatRemaining(ms) { + if (ms <= 0) + return I18n.tr("Off"); + const totalMinutes = Math.ceil(ms / 60000); + if (totalMinutes < 60) + return I18n.tr("%1 min left").arg(totalMinutes); + const hours = Math.floor(totalMinutes / 60); + const mins = totalMinutes - hours * 60; + if (mins === 0) + return I18n.tr("%1 h left").arg(hours); + return I18n.tr("%1 h %2 m left").arg(hours).arg(mins); + } + + function formatUntilTimestamp(ts) { + if (!ts) + return ""; + const d = new Date(ts); + const hours = d.getHours(); + const minutes = d.getMinutes(); + const use24h = (typeof SettingsData !== "undefined") ? SettingsData.use24HourClock : true; + if (use24h) { + return _pad2(hours) + ":" + _pad2(minutes); + } + const suffix = hours >= 12 ? "PM" : "AM"; + const h12 = ((hours + 11) % 12) + 1; + return h12 + ":" + _pad2(minutes) + " " + suffix; + } + + function minutesUntilTomorrowMorning() { + const now = new Date(); + const target = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8, 0, 0, 0); + return Math.max(1, Math.round((target.getTime() - now.getTime()) / 60000)); + } + + readonly property var presetOptions: [ + { + "label": I18n.tr("For 15 minutes"), + "minutes": 15 + }, + { + "label": I18n.tr("For 30 minutes"), + "minutes": 30 + }, + { + "label": I18n.tr("For 1 hour"), + "minutes": 60 + }, + { + "label": I18n.tr("For 3 hours"), + "minutes": 180 + }, + { + "label": I18n.tr("For 8 hours"), + "minutes": 480 + }, + { + "label": I18n.tr("Until tomorrow, 8:00 AM"), + "minutesFn": true + }, + { + "label": I18n.tr("Until I turn it off"), + "minutes": 0 + } + ] + + function selectPreset(option) { + let minutes = option.minutes; + if (option.minutesFn) { + minutes = minutesUntilTomorrowMorning(); + } + SessionData.setDoNotDisturb(true, minutes); + root.dismissed(); + } + + function turnOff() { + SessionData.setDoNotDisturb(false); + root.dismissed(); + } + + implicitWidth: Math.max(220, menuColumn.implicitWidth + Theme.spacingM * 2) + implicitHeight: menuColumn.implicitHeight + Theme.spacingM * 2 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + + Column { + id: menuColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: SessionData.doNotDisturb ? "notifications_off" : "notifications_paused" + size: Theme.iconSize - 2 + color: SessionData.doNotDisturb ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - parent.spacing + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: I18n.tr("Do Not Disturb") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + visible: root.currentlyActive + text: { + if (SessionData.doNotDisturbUntil > 0) { + return root.formatRemaining(root.currentRemainingMs) + " · " + I18n.tr("until %1").arg(root.formatUntilTimestamp(SessionData.doNotDisturbUntil)); + } + return I18n.tr("On indefinitely"); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + elide: Text.ElideRight + width: parent.width + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15) + } + + Repeater { + model: root.presetOptions + + Rectangle { + id: optionRect + required property var modelData + width: menuColumn.width + height: 32 + radius: Theme.cornerRadius + color: optionArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: optionRect.modelData.label + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + } + + MouseArea { + id: optionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.selectPreset(optionRect.modelData) + } + } + } + + Rectangle { + visible: root.currentlyActive + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15) + } + + Rectangle { + visible: root.currentlyActive + width: menuColumn.width + height: 32 + radius: Theme.cornerRadius + color: offArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.14) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "notifications_active" + size: Theme.iconSizeSmall + color: offArea.containsMouse ? Theme.error : Theme.surfaceText + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Turn off now") + font.pixelSize: Theme.fontSizeSmall + color: offArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: offArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.turnOff() + } + } + } +} diff --git a/quickshell/Modules/Notifications/Center/DndDurationPopup.qml b/quickshell/Modules/Notifications/Center/DndDurationPopup.qml new file mode 100644 index 00000000..cb2c4d7f --- /dev/null +++ b/quickshell/Modules/Notifications/Center/DndDurationPopup.qml @@ -0,0 +1,88 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + WindowBlur { + targetWindow: root + blurX: menu.x + blurY: menu.y + blurWidth: root.visible ? menu.width : 0 + blurHeight: root.visible ? menu.height : 0 + blurRadius: Theme.cornerRadius + } + + WlrLayershell.namespace: "dms:dnd-duration-menu" + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + property point anchorPos: Qt.point(0, 0) + property string anchorEdge: "top" + visible: false + + function showAt(x, y, targetScreen, edge) { + if (targetScreen) + root.screen = targetScreen; + anchorPos = Qt.point(x, y); + anchorEdge = edge || "top"; + visible = true; + } + + function closeMenu() { + visible = false; + } + + Connections { + target: PopoutManager + function onPopoutOpening() { + root.closeMenu(); + } + } + + MouseArea { + anchors.fill: parent + z: 0 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.closeMenu() + } + + DndDurationMenu { + id: menu + z: 1 + visible: root.visible + + x: { + const left = 10; + const right = root.width - width - 10; + const want = root.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + y: { + switch (root.anchorEdge) { + case "bottom": + return Math.max(10, root.anchorPos.y - height); + case "left": + case "right": + return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2)); + default: + return Math.min(root.height - height - 10, root.anchorPos.y); + } + } + + onDismissed: root.closeMenu() + } +} diff --git a/quickshell/Modules/Notifications/Center/NotificationHeader.qml b/quickshell/Modules/Notifications/Center/NotificationHeader.qml index 201e7169..c81bb27e 100644 --- a/quickshell/Modules/Notifications/Center/NotificationHeader.qml +++ b/quickshell/Modules/Notifications/Center/NotificationHeader.qml @@ -9,12 +9,18 @@ Item { property var keyboardController: null property bool showSettings: false property int currentTab: 0 + property bool showDndMenu: false onCurrentTabChanged: { if (currentTab === 1 && !SettingsData.notificationHistoryEnabled) currentTab = 0; } + onShowSettingsChanged: { + if (showSettings) + showDndMenu = false; + } + Connections { target: SettingsData function onNotificationHistoryEnabledChanged() { @@ -59,8 +65,31 @@ Item { iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText buttonSize: Theme.iconSize + Theme.spacingS anchors.verticalCenter: parent.verticalCenter - onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) - onEntered: sharedTooltip.show(I18n.tr("Do Not Disturb"), doNotDisturbButton, 0, 0, "bottom") + onClicked: { + if (SessionData.doNotDisturb) { + SessionData.setDoNotDisturb(false); + return; + } + root.showDndMenu = !root.showDndMenu; + if (root.showDndMenu) + root.showSettings = false; + } + onEntered: sharedTooltip.show(SessionData.doNotDisturb ? I18n.tr("Turn off Do Not Disturb") : I18n.tr("Do Not Disturb"), doNotDisturbButton, 0, 0, "bottom") + onExited: sharedTooltip.hide() + } + + DankActionButton { + id: dndScheduleButton + iconName: root.showDndMenu ? "expand_less" : "schedule" + iconColor: root.showDndMenu ? Theme.primary : Theme.surfaceText + buttonSize: Theme.iconSize + Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + onClicked: { + root.showDndMenu = !root.showDndMenu; + if (root.showDndMenu) + root.showSettings = false; + } + onEntered: sharedTooltip.show(I18n.tr("Silence for a while"), dndScheduleButton, 0, 0, "bottom") onExited: sharedTooltip.hide() } } @@ -139,6 +168,13 @@ Item { } } + DndDurationMenu { + id: dndMenu + width: parent.width + visible: root.showDndMenu + onDismissed: root.showDndMenu = false + } + DankButtonGroup { id: tabGroup width: parent.width diff --git a/quickshell/Modules/ProcessList/ProcessContextMenu.qml b/quickshell/Modules/ProcessList/ProcessContextMenu.qml index 7c48b5a9..0acee70f 100644 --- a/quickshell/Modules/ProcessList/ProcessContextMenu.qml +++ b/quickshell/Modules/ProcessList/ProcessContextMenu.qml @@ -185,7 +185,7 @@ Popup { } contentItem: Rectangle { - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + color: BlurService.enabled ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) radius: Theme.cornerRadius border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.width: BlurService.enabled ? BlurService.borderWidth : 1 @@ -274,7 +274,7 @@ Popup { } if (isSelected) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); - return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"; + return menuItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; } opacity: modelData.enabled ? 1 : 0.5 diff --git a/quickshell/Widgets/DankTooltip.qml b/quickshell/Widgets/DankTooltip.qml index 0e57de9f..ed5f04e5 100644 --- a/quickshell/Widgets/DankTooltip.qml +++ b/quickshell/Widgets/DankTooltip.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import qs.Common +import qs.Services PanelWindow { id: root @@ -71,10 +72,10 @@ PanelWindow { Rectangle { anchors.fill: parent - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + color: BlurService.enabled ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) radius: Theme.cornerRadius - border.width: 1 - border.color: Theme.outlineMedium + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium Text { id: textContent diff --git a/quickshell/Widgets/DankTooltipV2.qml b/quickshell/Widgets/DankTooltipV2.qml index 6cd53c8d..ab6a0c06 100644 --- a/quickshell/Widgets/DankTooltipV2.qml +++ b/quickshell/Widgets/DankTooltipV2.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import qs.Common +import qs.Services Item { id: root @@ -111,10 +112,10 @@ Item { dim: false background: Rectangle { - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + color: BlurService.enabled ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) radius: Theme.cornerRadius - border.width: 1 - border.color: Theme.outlineMedium + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium } contentItem: Text {