From 6d76f0b476319d8a9040d6e4025cb98248e6ca1d Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 3 Jan 2026 15:00:12 -0500 Subject: [PATCH] power: add fade to monitor off option fixes #558 --- quickshell/Common/SettingsData.qml | 2 + quickshell/Common/settings/SettingsSpec.js | 2 + quickshell/DMSShell.qml | 40 +++++++ quickshell/Modules/Lock/FadeToDpmsWindow.qml | 102 ++++++++++++++++++ quickshell/Modules/Lock/Lock.qml | 32 +++--- quickshell/Modules/Settings/PowerSleepTab.qml | 37 ++++++- quickshell/Services/IdleService.qml | 19 +++- 7 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 quickshell/Modules/Lock/FadeToDpmsWindow.qml diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 08981d6c..321cd0b7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -278,6 +278,8 @@ Singleton { property bool loginctlLockIntegration: true property bool fadeToLockEnabled: false property int fadeToLockGracePeriod: 5 + property bool fadeToDpmsEnabled: false + property int fadeToDpmsGracePeriod: 5 property string launchPrefix: "" property var brightnessDevicePins: ({}) property var wifiNetworkPins: ({}) diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 5b7d94db..cc949b65 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -168,6 +168,8 @@ var SPEC = { loginctlLockIntegration: { def: true }, fadeToLockEnabled: { def: false }, fadeToLockGracePeriod: { def: 5 }, + fadeToDpmsEnabled: { def: false }, + fadeToDpmsGracePeriod: { def: 5 }, launchPrefix: { def: "" }, brightnessDevicePins: { def: {} }, wifiNetworkPins: { def: {} }, diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index c40c1fee..c5ea0296 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -104,6 +104,46 @@ Item { } } + Variants { + model: Quickshell.screens + + delegate: Loader { + id: fadeDpmsWindowLoader + required property var modelData + active: SettingsData.fadeToDpmsEnabled + asynchronous: false + + sourceComponent: FadeToDpmsWindow { + screen: fadeDpmsWindowLoader.modelData + + onFadeCompleted: { + IdleService.requestMonitorOff(); + } + + onFadeCancelled: { + console.log("Fade to DPMS cancelled by user on screen:", fadeDpmsWindowLoader.modelData.name); + } + } + + Connections { + target: IdleService + enabled: fadeDpmsWindowLoader.item !== null + + function onFadeToDpmsRequested() { + if (fadeDpmsWindowLoader.item) { + fadeDpmsWindowLoader.item.startFade(); + } + } + + function onCancelFadeToDpms() { + if (fadeDpmsWindowLoader.item) { + fadeDpmsWindowLoader.item.cancelFade(); + } + } + } + } + } + Repeater { id: dankBarRepeater model: ScriptModel { diff --git a/quickshell/Modules/Lock/FadeToDpmsWindow.qml b/quickshell/Modules/Lock/FadeToDpmsWindow.qml new file mode 100644 index 00000000..04868909 --- /dev/null +++ b/quickshell/Modules/Lock/FadeToDpmsWindow.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common + +PanelWindow { + id: root + + property bool active: false + + signal fadeCompleted + signal fadeCancelled + + visible: active + color: "transparent" + + WlrLayershell.namespace: "dms:fade-to-dpms" + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: active ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + + anchors { + left: true + right: true + top: true + bottom: true + } + + Rectangle { + id: fadeOverlay + anchors.fill: parent + color: "black" + opacity: 0 + + onOpacityChanged: { + if (opacity >= 0.99 && root.active) { + root.fadeCompleted(); + } + } + } + + SequentialAnimation { + id: fadeSeq + running: false + + NumberAnimation { + target: fadeOverlay + property: "opacity" + from: 0.0 + to: 1.0 + duration: SettingsData.fadeToDpmsGracePeriod * 1000 + easing.type: Easing.OutCubic + } + } + + function startFade() { + if (!SettingsData.fadeToDpmsEnabled) + return; + active = true; + fadeOverlay.opacity = 0.0; + fadeSeq.stop(); + fadeSeq.start(); + } + + function cancelFade() { + fadeSeq.stop(); + fadeOverlay.opacity = 0.0; + active = false; + fadeCancelled(); + } + + MouseArea { + anchors.fill: parent + enabled: root.active + onClicked: root.cancelFade() + onPressed: root.cancelFade() + } + + FocusScope { + anchors.fill: parent + focus: root.active + + Keys.onPressed: event => { + root.cancelFade(); + event.accepted = true; + } + } + + Component.onCompleted: { + if (active) { + forceActiveFocus(); + } + } + + onActiveChanged: { + if (active) { + forceActiveFocus(); + } + } +} diff --git a/quickshell/Modules/Lock/Lock.qml b/quickshell/Modules/Lock/Lock.qml index 25ab3201..3b1b68d2 100644 --- a/quickshell/Modules/Lock/Lock.qml +++ b/quickshell/Modules/Lock/Lock.qml @@ -23,15 +23,12 @@ Scope { Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLock]); return; } + shouldLock = true; if (!processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) { DMSService.lockSession(response => { - if (response.error) { - console.warn("Lock: Failed to call loginctl.lock:", response.error); - shouldLock = true; - } + if (response.error) + console.warn("Lock: loginctl.lock failed:", response.error); }); - } else { - shouldLock = true; } } @@ -81,6 +78,11 @@ Scope { locked: shouldLock + onLockedChanged: { + if (locked) + dpmsReapplyTimer.start(); + } + WlSessionLockSurface { id: lockSurface @@ -120,15 +122,12 @@ Scope { target: "lock" function lock() { - if (!root.processingExternalEvent && SettingsData.loginctlLockIntegration && DMSService.isConnected) { + root.shouldLock = true; + if (SettingsData.loginctlLockIntegration && DMSService.isConnected) { DMSService.lockSession(response => { - if (response.error) { - console.warn("Lock: Failed to call loginctl.lock:", response.error); - root.shouldLock = true; - } + if (response.error) + console.warn("Lock: loginctl.lock failed:", response.error); }); - } else { - root.shouldLock = true; } } @@ -140,4 +139,11 @@ Scope { return sessionLock.locked; } } + + Timer { + id: dpmsReapplyTimer + interval: 100 + repeat: false + onTriggered: IdleService.reapplyDpmsIfNeeded() + } } diff --git a/quickshell/Modules/Settings/PowerSleepTab.qml b/quickshell/Modules/Settings/PowerSleepTab.qml index 28d79efc..6c713b1a 100644 --- a/quickshell/Modules/Settings/PowerSleepTab.qml +++ b/quickshell/Modules/Settings/PowerSleepTab.qml @@ -72,6 +72,15 @@ Item { onToggled: checked => SettingsData.set("fadeToLockEnabled", checked) } + SettingsToggleRow { + settingKey: "fadeToDpmsEnabled" + tags: ["fade", "dpms", "monitor", "screen", "idle", "grace period"] + text: I18n.tr("Fade to monitor off") + description: I18n.tr("Gradually fade the screen before turning off monitors with a configurable grace period") + checked: SettingsData.fadeToDpmsEnabled + onToggled: checked => SettingsData.set("fadeToDpmsEnabled", checked) + } + SettingsToggleRow { settingKey: "lockBeforeSuspend" tags: ["lock", "suspend", "sleep", "security"] @@ -89,7 +98,7 @@ Item { property var periodOptions: ["1 second", "2 seconds", "3 seconds", "4 seconds", "5 seconds", "10 seconds", "15 seconds", "20 seconds", "30 seconds"] property var periodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30] - text: I18n.tr("Fade grace period") + text: I18n.tr("Lock fade grace period") options: periodOptions visible: SettingsData.fadeToLockEnabled enabled: SettingsData.fadeToLockEnabled @@ -107,6 +116,32 @@ Item { SettingsData.set("fadeToLockGracePeriod", periodValues[index]); } } + + SettingsDropdownRow { + id: fadeDpmsGracePeriodDropdown + settingKey: "fadeToDpmsGracePeriod" + tags: ["fade", "grace", "period", "timeout", "dpms", "monitor"] + property var periodOptions: ["1 second", "2 seconds", "3 seconds", "4 seconds", "5 seconds", "10 seconds", "15 seconds", "20 seconds", "30 seconds"] + property var periodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30] + + text: I18n.tr("Monitor fade grace period") + options: periodOptions + visible: SettingsData.fadeToDpmsEnabled + enabled: SettingsData.fadeToDpmsEnabled + + Component.onCompleted: { + const currentPeriod = SettingsData.fadeToDpmsGracePeriod; + const index = periodValues.indexOf(currentPeriod); + currentValue = index >= 0 ? periodOptions[index] : "5 seconds"; + } + + onValueChanged: value => { + const index = periodOptions.indexOf(value); + if (index < 0) + return; + SettingsData.set("fadeToDpmsGracePeriod", periodValues[index]); + } + } SettingsDropdownRow { id: powerProfileDropdown settingKey: "powerProfile" diff --git a/quickshell/Services/IdleService.qml b/quickshell/Services/IdleService.qml index da014986..f452617d 100644 --- a/quickshell/Services/IdleService.qml +++ b/quickshell/Services/IdleService.qml @@ -53,6 +53,8 @@ Singleton { signal lockRequested signal fadeToLockRequested signal cancelFadeToLock + signal fadeToDpmsRequested + signal cancelFadeToDpms signal requestMonitorOff signal requestMonitorOn signal requestSuspend @@ -61,11 +63,17 @@ Singleton { property var lockMonitor: null property var suspendMonitor: null property var lockComponent: null + property bool monitorsOff: false function wake() { requestMonitorOn(); } + function reapplyDpmsIfNeeded() { + if (monitorsOff) + CompositorService.powerOffMonitors(); + } + function createIdleMonitors() { if (!idleMonitorAvailable) { console.info("IdleService: IdleMonitor not available, skipping creation"); @@ -90,8 +98,15 @@ Singleton { monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout); monitorOffMonitor.isIdleChanged.connect(function () { if (monitorOffMonitor.isIdle) { - root.requestMonitorOff(); + if (SettingsData.fadeToDpmsEnabled) { + root.fadeToDpmsRequested(); + } else { + root.requestMonitorOff(); + } } else { + if (SettingsData.fadeToDpmsEnabled) { + root.cancelFadeToDpms(); + } root.requestMonitorOn(); } }); @@ -131,10 +146,12 @@ Singleton { Connections { target: root function onRequestMonitorOff() { + monitorsOff = true; CompositorService.powerOffMonitors(); } function onRequestMonitorOn() { + monitorsOff = false; CompositorService.powerOnMonitors(); }