From 4f2d319a356a8181191ebf6c030ff47150d5b353 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Thu, 24 Sep 2026 12:49:45 -0400 Subject: [PATCH] Add idle timeouts to the Quick Settings General tab --- docs/widgets.md | 11 ++- .../panels/quick-settings/GeneralTab.qml | 59 +++++++++++++ .../panels/quick-settings/QuickSettings.qml | 46 ++++++---- .../panels/quick-settings/TimeoutRow.qml | 88 +++++++++++++++++++ 4 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 shell/plugins/panels/quick-settings/GeneralTab.qml create mode 100644 shell/plugins/panels/quick-settings/TimeoutRow.qml diff --git a/docs/widgets.md b/docs/widgets.md index 7387dd3..70a55ff 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -9,7 +9,7 @@ autostarted. | Was | Is | Note | | --- | --- | --- | -| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 13 files, none over 350 lines, with four tabs | +| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 15 files, none over 350 lines, with four tabs | | `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature; the probes and bars are `Ui/SystemStats.qml` and `Ui/MetricBar.qml`, shared with the Performance tab | | `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files | | `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list | @@ -41,13 +41,20 @@ running `pamixer` and `playerctl` every second. | Tab | Holds | | --- | --- | -| General | the calendar and the brightness slider | +| General | the calendar, the brightness slider, and the screensaver and lock timeouts | | Sound & Music | output and microphone volume (click the icon to mute), the media row with its controls, and buttons for the audio panel (picking devices) and the pavucontrol mixer | | Performance | CPU, memory, disk and temperature bars, the load average, the power profile (Power saver, Balanced, Performance, whichever the machine offers), and a button that opens btop | | Quick Settings | the toggle tiles: Wi-Fi, Bluetooth, Wallpaper, Silence, Night Light, Record, Stay Awake, Pick Color, Theme, Tablet Lock | The weather card beside it is not part of the tabs and stays visible. +The two timeouts on the General tab are the `idle.screensaver` and `idle.lock` +values in `~/.config/blob/shell.json`, in seconds. The minus and plus buttons +step through 1, 2, 5, 10, 15, 30 and 60 minutes (and 2 hours for the lock), +starting from whatever the file holds, and the idle service picks the new value +up straight away. A value set by hand that is not a preset is shown as it is. +While Stay Awake is on, the tab says so, since neither timeout runs then. + ## Opening them | Panel | Opened by | diff --git a/shell/plugins/panels/quick-settings/GeneralTab.qml b/shell/plugins/panels/quick-settings/GeneralTab.qml new file mode 100644 index 0000000..ca7da26 --- /dev/null +++ b/shell/plugins/panels/quick-settings/GeneralTab.qml @@ -0,0 +1,59 @@ +import QtQuick +import qs.Commons + +Column { + id: root + + property bool polling: false + property int brightness: 0 + property int screensaverSeconds: 0 + property int lockSeconds: 0 + property bool stayAwake: false + + signal brightnessRequested(int value) + signal timeoutRequested(string key, int seconds) + + spacing: Style.spaceReal(12) + + Calendar { + width: parent.width + polling: root.polling + } + + SliderRow { + width: parent.width + icon: "" + value: root.brightness + onValueRequested: function(next) { root.brightnessRequested(next) } + } + + TimeoutRow { + width: parent.width + icon: "" + label: "Screensaver after" + seconds: root.screensaverSeconds + presetMinutes: [1, 2, 5, 10, 15, 30, 60] + onSecondsRequested: function(seconds) { root.timeoutRequested("screensaver", seconds) } + } + + TimeoutRow { + width: parent.width + icon: "" + label: "Lock after" + seconds: root.lockSeconds + presetMinutes: [1, 2, 5, 10, 15, 30, 60, 120] + onSecondsRequested: function(seconds) { root.timeoutRequested("lock", seconds) } + } + + Text { + width: parent.width + visible: root.stayAwake + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: "Stay Awake is on, so neither starts until it is turned off." + textFormat: Text.PlainText + color: Util.alpha(Color.foreground, 0.6) + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } +} diff --git a/shell/plugins/panels/quick-settings/QuickSettings.qml b/shell/plugins/panels/quick-settings/QuickSettings.qml index e5462c4..8f835db 100644 --- a/shell/plugins/panels/quick-settings/QuickSettings.qml +++ b/shell/plugins/panels/quick-settings/QuickSettings.qml @@ -27,6 +27,11 @@ Item { readonly property bool performanceVisible: opened && currentTab === "performance" readonly property bool quickVisible: opened && currentTab === "quick" + readonly property var idleConfig: root.shell && root.shell.shellConfig && root.shell.shellConfig.idle + ? root.shell.shellConfig.idle : ({}) + readonly property int screensaverSeconds: root.timeoutSeconds(idleConfig.screensaver, 150) + readonly property int lockSeconds: root.timeoutSeconds(idleConfig.lock, 300) + readonly property int barInset: { var bar = root.shell ? root.shell.bar : null if (!bar || bar.barHidden === true) return 0 @@ -78,6 +83,19 @@ Item { mediaValue.refresh() } + function timeoutSeconds(value, fallback) { + var seconds = Number(value) + return isFinite(seconds) && seconds >= 0 ? Math.floor(seconds) : fallback + } + + function setIdleTimeout(key, seconds) { + if (!root.shell || typeof root.shell.mutateShellConfig !== "function") return + root.shell.mutateShellConfig(function(config) { + if (!config.idle || typeof config.idle !== "object") config.idle = {} + config.idle[key] = seconds + }) + } + function toggleAndRefresh(command) { root.run(command) stateSettleTimer.restart() @@ -109,7 +127,7 @@ Item { PolledFlag { id: stayAwakeFlag - polling: root.quickVisible + polling: root.quickVisible || root.generalVisible command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off" } @@ -256,25 +274,19 @@ Item { onTabRequested: function(tabId) { root.currentTab = tabId } } - Column { + GeneralTab { width: parent.width visible: root.currentTab === "general" - spacing: Style.spaceReal(12) - - Calendar { - width: parent.width - polling: root.generalVisible - } - - SliderRow { - width: parent.width - icon: "\uf185" - value: Number(brightnessValue.value) || 0 - onValueRequested: function(next) { - root.run("brightnessctl set " + next + "%") - brightnessValue.value = String(next) - } + polling: root.generalVisible + brightness: Number(brightnessValue.value) || 0 + screensaverSeconds: root.screensaverSeconds + lockSeconds: root.lockSeconds + stayAwake: stayAwakeFlag.value + onBrightnessRequested: function(next) { + root.run("brightnessctl set " + next + "%") + brightnessValue.value = String(next) } + onTimeoutRequested: function(key, seconds) { root.setIdleTimeout(key, seconds) } } SoundTab { diff --git a/shell/plugins/panels/quick-settings/TimeoutRow.qml b/shell/plugins/panels/quick-settings/TimeoutRow.qml new file mode 100644 index 0000000..96c1110 --- /dev/null +++ b/shell/plugins/panels/quick-settings/TimeoutRow.qml @@ -0,0 +1,88 @@ +import QtQuick +import qs.Commons +import qs.Ui + +Item { + id: root + + property string icon: "" + property string label: "" + property int seconds: 0 + property var presetMinutes: [] + + signal secondsRequested(int seconds) + + implicitHeight: Math.max(labelText.implicitHeight, controls.implicitHeight) + + function formatSeconds(value) { + if (value >= 3600 && value % 3600 === 0) return (value / 3600) + " h" + if (value % 60 === 0) return (value / 60) + " min" + return (Math.round(value / 6) / 10) + " min" + } + + function step(delta) { + var minutes = root.seconds / 60 + var presets = root.presetMinutes + var next = -1 + if (delta > 0) { + for (var i = 0; i < presets.length; i++) + if (presets[i] > minutes) { next = presets[i]; break } + } else { + for (var j = presets.length - 1; j >= 0; j--) + if (presets[j] < minutes) { next = presets[j]; break } + } + if (next > 0) root.secondsRequested(next * 60) + } + + Text { + id: iconText + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Style.spaceReal(22) + text: root.icon + textFormat: Text.PlainText + color: Color.magenta + font.family: Style.font.family + font.pixelSize: Style.font.body + } + + Text { + id: labelText + anchors.left: iconText.right + anchors.leftMargin: Style.spaceReal(10) + anchors.verticalCenter: parent.verticalCenter + text: root.label + textFormat: Text.PlainText + color: Color.foreground + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } + + Row { + id: controls + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Style.spaceReal(8) + + CardIconButton { + icon: "-" + onActivated: root.step(-1) + } + + Text { + anchors.verticalCenter: parent.verticalCenter + width: Style.spaceReal(52) + horizontalAlignment: Text.AlignHCenter + text: root.formatSeconds(root.seconds) + textFormat: Text.PlainText + color: Color.foreground + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } + + CardIconButton { + icon: "+" + onActivated: root.step(1) + } + } +}