From 597ba597e501f9c0ae5866762da7ae71199c5abf Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 2 Jul 2026 17:13:18 -0400 Subject: [PATCH] feat(settings): implement greeter & lockscreen customization options - New option to customize your font & wallpaper on the LockScreen - New floating sync button if missed to explicitly sync on greeter changes --- quickshell/Common/SettingsData.qml | 34 ++- quickshell/Common/settings/Processes.qml | 35 +--- quickshell/Common/settings/SettingsSpec.js | 26 ++- quickshell/Modules/Lock/LockScreenContent.qml | 77 +++---- quickshell/Modules/Settings/GreeterTab.qml | 196 +++++++++--------- quickshell/Modules/Settings/LockScreenTab.qml | 51 +++++ .../Widgets/SettingsFontDropdownRow.qml | 33 +++ .../Widgets/SettingsWallpaperPicker.qml | 80 +++++++ .../translations/settings_search_index.json | 61 ++---- 9 files changed, 372 insertions(+), 221 deletions(-) create mode 100644 quickshell/Modules/Settings/Widgets/SettingsFontDropdownRow.qml create mode 100644 quickshell/Modules/Settings/Widgets/SettingsWallpaperPicker.qml diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index b5ad22b66..5ecdf1972 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -494,6 +494,8 @@ Singleton { property string greeterLockDateFormat: "" property string greeterFontFamily: "" property string greeterWallpaperFillMode: "" + property bool greeterSyncPending: false + property var greeterSyncBaseline: ({}) property int mediaSize: 1 property string appLauncherViewMode: "list" @@ -891,6 +893,9 @@ Singleton { property bool lockScreenVideoEnabled: false property string lockScreenVideoPath: "" property bool lockScreenVideoCycling: false + property string lockScreenWallpaperPath: "" + property string lockScreenWallpaperFillMode: "" + property string lockScreenFontFamily: "" property bool hideBrightnessSlider: false property int notificationTimeoutLow: 5000 @@ -1677,6 +1682,32 @@ Singleton { }); } + function markGreeterSyncPending(who, key, oldValue) { + if (isGreeterMode) + return; + if (!(key in greeterSyncBaseline)) { + var baseline = greeterSyncBaseline; + baseline[key] = oldValue; + greeterSyncBaseline = baseline; + } + greeterSyncPending = true; + } + + function clearGreeterSyncPending() { + greeterSyncBaseline = {}; + greeterSyncPending = false; + saveSettings(); + } + + function revertGreeterSyncPending() { + for (var key in greeterSyncBaseline) { + root[key] = greeterSyncBaseline[key]; + } + greeterSyncBaseline = {}; + greeterSyncPending = false; + saveSettings(); + } + readonly property var _hooks: ({ "applyStoredTheme": applyStoredTheme, "regenSystemThemes": regenSystemThemes, @@ -1685,7 +1716,8 @@ Singleton { "updateBarConfigs": updateBarConfigs, "updateCompositorCursor": updateCompositorCursor, "scheduleAuthApply": scheduleAuthApply, - "scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync + "scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync, + "markGreeterSyncPending": markGreeterSyncPending }) function set(key, value) { diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index 8b2ac865f..f7e895859 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -332,7 +332,6 @@ Singleton { property bool greeterAutoLoginSyncRerunRequested: false property string greeterAutoLoginSyncStdout: "" property string greeterAutoLoginSyncStderr: "" - property string greeterAutoLoginSyncTerminalFallbackStderr: "" function scheduleGreeterAutoLoginSync() { if (!settingsRoot || settingsRoot.isGreeterMode) @@ -355,15 +354,16 @@ Singleton { greeterAutoLoginSyncRerunRequested = false; greeterAutoLoginSyncStdout = ""; greeterAutoLoginSyncStderr = ""; - greeterAutoLoginSyncTerminalFallbackStderr = ""; greeterAutoLoginSyncRunning = true; greeterAutoLoginSyncSudoProbeProcess.running = true; } - function launchGreeterAutoLoginSyncTerminalFallback(details) { - ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync"); - greeterAutoLoginSyncTerminalFallbackStderr = ""; - greeterAutoLoginSyncTerminalFallbackProcess.running = true; + function deferGreeterAutoLoginSyncToPill(details) { + ToastService.dismissCategory("greeter-autologin-sync"); + if (settingsRoot) + settingsRoot.set("greeterSyncPending", true); + ToastService.showWarning(I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync"); + finishGreeterAutoLoginSync(); } function greeterAutoLoginSyncSuccessToast(details) { @@ -559,7 +559,7 @@ Singleton { details = out; if (err !== "") details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err; - root.launchGreeterAutoLoginSyncTerminalFallback(details); + root.deferGreeterAutoLoginSyncToPill(details); } } @@ -575,26 +575,7 @@ Singleton { return; } - root.launchGreeterAutoLoginSyncTerminalFallback(); - } - } - - property var greeterAutoLoginSyncTerminalFallbackProcess: Process { - command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin"] - running: false - - stderr: StdioCollector { - onStreamFinished: root.greeterAutoLoginSyncTerminalFallbackStderr = text || "" - } - - onExited: exitCode => { - if (exitCode === 0) { - root.greeterAutoLoginSyncSuccessToast(""); - } else { - let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim(); - ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin", "greeter-autologin-sync"); - } - root.finishGreeterAutoLoginSync(); + root.deferGreeterAutoLoginSyncToPill(""); } } diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index fe09468fb..d4b9ea75a 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -218,18 +218,20 @@ var SPEC = { centeringMode: { def: "index" }, clockDateFormat: { def: "" }, lockDateFormat: { def: "" }, - greeterRememberLastSession: { def: true }, - greeterRememberLastUser: { def: true }, + greeterRememberLastSession: { def: true, onChange: "markGreeterSyncPending" }, + greeterRememberLastUser: { def: true, onChange: "markGreeterSyncPending" }, greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" }, greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" }, greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" }, - greeterWallpaperPath: { def: "" }, - greeterUse24HourClock: { def: true }, - greeterShowSeconds: { def: false }, - greeterPadHours12Hour: { def: false }, - greeterLockDateFormat: { def: "" }, - greeterFontFamily: { def: "" }, - greeterWallpaperFillMode: { def: "" }, + greeterWallpaperPath: { def: "", onChange: "markGreeterSyncPending" }, + greeterUse24HourClock: { def: true, onChange: "markGreeterSyncPending" }, + greeterShowSeconds: { def: false, onChange: "markGreeterSyncPending" }, + greeterPadHours12Hour: { def: false, onChange: "markGreeterSyncPending" }, + greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" }, + greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" }, + greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" }, + greeterSyncPending: { def: false }, + greeterSyncBaseline: { def: {} }, mediaSize: { def: 1 }, appLauncherViewMode: { def: "list" }, @@ -453,6 +455,9 @@ var SPEC = { lockScreenVideoEnabled: { def: false }, lockScreenVideoPath: { def: "" }, lockScreenVideoCycling: { def: false }, + lockScreenWallpaperPath: { def: "" }, + lockScreenWallpaperFillMode: { def: "" }, + lockScreenFontFamily: { def: "" }, hideBrightnessSlider: { def: false }, notificationTimeoutLow: { def: 5000 }, @@ -659,10 +664,11 @@ function getValidKeys() { function set(root, key, value, saveFn, hooks) { if (!(key in SPEC)) return; if (value === undefined || value === null) value = SPEC[key].def; + var oldValue = root[key]; root[key] = value; var hookName = SPEC[key].onChange; if (hookName && hooks && hooks[hookName]) { - hooks[hookName](root); + hooks[hookName](root, key, oldValue); } saveFn(); } diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index 831039401..6dd30bca2 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -34,6 +34,16 @@ Item { property int hyprlandLayoutCount: 0 property bool lockerReadySent: false property bool lockerReadyArmed: false + readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== "" + readonly property string lockFontFamily: SettingsData.lockScreenFontFamily + + component ClockDigitText: StyledText { + font.pixelSize: 120 + font.weight: Font.Light + color: "white" + horizontalAlignment: Text.AlignHCenter + font.family: root.lockFontFamily !== "" ? root.lockFontFamily : resolvedFontFamily + } signal unlockRequested @@ -190,6 +200,8 @@ Item { Loader { anchors.fill: parent active: { + if (root.hasCustomWallpaper) + return false; var currentWallpaper = SessionData.getMonitorWallpaper(screenName); return !currentWallpaper || (currentWallpaper && currentWallpaper.startsWith("#")); } @@ -205,10 +217,16 @@ Item { anchors.fill: parent readonly property string wallpaperSource: { + if (root.hasCustomWallpaper) + return root.encodeFileUrl(SettingsData.lockScreenWallpaperPath); var w = SessionData.getMonitorWallpaper(screenName); return (w && !w.startsWith("#")) ? encodeFileUrl(w) : ""; } - readonly property string fillModeName: SessionData.getMonitorWallpaperFillMode(screenName) + readonly property string fillModeName: { + if (SettingsData.lockScreenWallpaperFillMode !== "") + return SettingsData.lockScreenWallpaperFillMode; + return root.hasCustomWallpaper ? "Fill" : SessionData.getMonitorWallpaperFillMode(root.screenName); + } active: wallpaperSource !== "" asynchronous: false @@ -332,91 +350,55 @@ Item { } property bool hasSeconds: timeParts.length > 2 - StyledText { + ClockDigitText { width: 75 text: clockText.hours.length > 1 ? clockText.hours[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter } - StyledText { + ClockDigitText { width: 75 text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter } - StyledText { + ClockDigitText { text: ":" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" } - StyledText { + ClockDigitText { width: 75 text: clockText.minutes.length > 0 ? clockText.minutes[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter } - StyledText { + ClockDigitText { width: 75 text: clockText.minutes.length > 1 ? clockText.minutes[1] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter } - StyledText { + ClockDigitText { text: clockText.hasSeconds ? ":" : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" visible: clockText.hasSeconds } - StyledText { + ClockDigitText { width: 75 text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter visible: clockText.hasSeconds } - StyledText { + ClockDigitText { width: 75 text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : "" - font.pixelSize: 120 - font.weight: Font.Light - color: "white" - horizontalAlignment: Text.AlignHCenter visible: clockText.hasSeconds } - StyledText { + ClockDigitText { width: 20 text: " " - font.pixelSize: 120 - font.weight: Font.Light - color: "white" visible: clockText.ampm !== "" } - StyledText { + ClockDigitText { text: clockText.ampm - font.pixelSize: 120 - font.weight: Font.Light - color: "white" visible: clockText.ampm !== "" } } @@ -435,6 +417,7 @@ Item { return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat); } font.pixelSize: Theme.fontSizeXLarge + font.family: root.lockFontFamily !== "" ? root.lockFontFamily : resolvedFontFamily color: "white" opacity: 0.9 } diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index b5736c704..0c61ca1db 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell.Io import qs.Common import qs.Modals.Common -import qs.Modals.FileBrowser +import qs.Services import qs.Widgets import qs.Modules.Settings.Widgets @@ -83,19 +83,6 @@ Item { id: greeterActionConfirm } - FileBrowserModal { - id: greeterWallpaperBrowserModal - browserTitle: I18n.tr("Select greeter background image") - browserIcon: "wallpaper" - browserType: "wallpaper" - showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"] - onFileSelected: path => { - SettingsData.set("greeterWallpaperPath", path); - close(); - } - } - property string greeterStatusText: "" property bool greeterStatusRunning: false property bool greeterSyncRunning: false @@ -107,8 +94,6 @@ Item { property string greeterSudoProbeStderr: "" property string greeterTerminalFallbackStderr: "" property bool greeterTerminalFallbackFromPrecheck: false - property var cachedFontFamilies: [] - property bool fontsEnumerated: false property bool greeterBinaryExists: false property bool greeterEnabled: false readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled @@ -201,26 +186,8 @@ Item { greeterTerminalFallbackProcess.running = true; } - function enumerateFonts() { - if (fontsEnumerated) - return; - var fonts = []; - var availableFonts = Qt.fontFamilies(); - for (var i = 0; i < availableFonts.length; i++) { - var fontName = availableFonts[i]; - if (fontName.startsWith(".")) - continue; - fonts.push(fontName); - } - fonts.sort(); - fonts.unshift("Default"); - cachedFontFamilies = fonts; - fontsEnumerated = true; - } - Component.onCompleted: { refreshAuthDetection(); - Qt.callLater(enumerateFonts); Qt.callLater(checkGreeterInstallState); } @@ -302,6 +269,8 @@ Item { if (err !== "") success = success + "\n\nstderr:\n" + err; root.greeterStatusText = success; + SettingsData.clearGreeterSyncPending(); + ToastService.showInfo(I18n.tr("Greeter sync complete")); } else { var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")"; if (out !== "") @@ -353,6 +322,7 @@ Item { if (exitCode === 0) { var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done."); root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched; + SettingsData.clearGreeterSyncPending(); return; } var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")"; @@ -424,12 +394,11 @@ Item { label: I18n.tr("Full Day & Month", "date format option") } ] - readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"] DankFlickable { anchors.fill: parent clip: true - contentHeight: mainColumn.height + Theme.spacingXL + contentHeight: mainColumn.height + Theme.spacingXL + (syncPendingPill.shown ? syncPendingPill.height + Theme.spacingL : 0) contentWidth: width Column { @@ -446,7 +415,7 @@ Item { settingKey: "greeterStatus" StyledText { - text: I18n.tr("Check sync status on demand. Sync (full) is for the main admin: it copies your theme to the login screen and sets up system greeter config. On multi-user systems, add other accounts in Settings → Users, then have each of them run dms greeter sync --profile after logging out and back in—not full sync. Authentication changes apply automatically.") + text: I18n.tr("Sync applies your theme and settings to the login screen. Other users should run dms greeter sync --profile instead of a full sync. Authentication changes apply automatically.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText width: parent.width @@ -567,22 +536,13 @@ Item { topPadding: Theme.spacingM } - SettingsDropdownRow { + SettingsFontDropdownRow { settingKey: "greeterFontFamily" tags: ["greeter", "font", "typography"] text: I18n.tr("Greeter font") description: I18n.tr("Font used on the login screen") - options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"] - currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default") - enableFuzzySearch: true - popupWidthOffset: 100 - maxPopupHeight: 400 - onValueChanged: value => { - if (value === "Default") - SettingsData.set("greeterFontFamily", ""); - else - SettingsData.set("greeterFontFamily", value); - } + currentFont: SettingsData.greeterFontFamily || "" + onFontSelected: family => SettingsData.set("greeterFontFamily", family) } StyledText { @@ -660,55 +620,16 @@ Item { wrapMode: Text.Wrap } - Row { + SettingsWallpaperPicker { width: parent.width - spacing: Theme.spacingS - - DankTextField { - id: greeterWallpaperPathField - width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS - placeholderText: I18n.tr("Use desktop wallpaper") - text: SettingsData.greeterWallpaperPath - backgroundColor: Theme.surfaceContainerHighest - onTextChanged: { - if (text !== SettingsData.greeterWallpaperPath) - SettingsData.set("greeterWallpaperPath", text); - } - } - - DankButton { - id: browseGreeterWallpaperButton - text: I18n.tr("Browse") - horizontalPadding: Theme.spacingL - onClicked: greeterWallpaperBrowserModal.open() - } - } - - SettingsDropdownRow { - settingKey: "greeterWallpaperFillMode" - tags: ["greeter", "wallpaper", "background", "fill"] - text: I18n.tr("Wallpaper fill mode") - description: I18n.tr("How the background image is scaled") - options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")) - currentValue: { - var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill"); - var idx = root._wallpaperFillModes.indexOf(mode); - return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode"); - } - onValueChanged: value => { - var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value); - if (idx >= 0) - SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]); - } - } - - StyledText { - text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - wrapMode: Text.Wrap - topPadding: Theme.spacingS + path: SettingsData.greeterWallpaperPath + fillMode: SettingsData.greeterWallpaperFillMode + fallbackFillMode: SettingsData.wallpaperFillMode || "Fill" + browserTitle: I18n.tr("Select greeter background image") + fillModeSettingKey: "greeterWallpaperFillMode" + fillModeTags: ["greeter", "wallpaper", "background", "fill"] + onPathSelected: path => SettingsData.set("greeterWallpaperPath", path) + onFillModeSelected: mode => SettingsData.set("greeterWallpaperFillMode", mode) } } @@ -762,7 +683,7 @@ Item { settingKey: "greeterDeps" StyledText { - text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Authentication changes apply automatically and may open a terminal when sudo authentication is required.") + text: I18n.tr("Requires greetd, dms-greeter, and your user in the greeter group (plus fprintd/pam_fprintd for fingerprint, pam_u2f for security keys). Auth changes apply automatically and may open a terminal for sudo.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText width: parent.width @@ -789,4 +710,83 @@ Item { } } } + + Rectangle { + id: syncPendingPill + + readonly property bool shown: SettingsData.greeterSyncPending && root.greeterInstalled + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: shown ? Theme.spacingL : Theme.spacingXS + width: pillRow.implicitWidth + Theme.spacingL * 2 + height: 44 + radius: height / 2 + color: Theme.primary + opacity: shown ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on anchors.bottomMargin { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: !root.greeterSyncRunning && !root.greeterInstallActionRunning + onClicked: root.runGreeterSync() + } + + Row { + id: pillRow + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + id: syncPillIcon + name: "sync" + size: Theme.iconSize - 4 + color: Theme.primaryText + anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + running: root.greeterSyncRunning && syncPendingPill.shown + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + onRunningChanged: { + if (!running) + syncPillIcon.rotation = 0; + } + } + } + + StyledText { + text: root.greeterSyncRunning ? I18n.tr("Syncing...") : I18n.tr("Sync to apply") + color: Theme.primaryText + font.pixelSize: Theme.fontSizeMedium + anchors.verticalCenter: parent.verticalCenter + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 6 + iconColor: Theme.primaryText + buttonSize: 28 + anchors.verticalCenter: parent.verticalCenter + onClicked: SettingsData.revertGreeterSyncPending() + } + } + } } diff --git a/quickshell/Modules/Settings/LockScreenTab.qml b/quickshell/Modules/Settings/LockScreenTab.qml index d86589cc5..664bfa5d1 100644 --- a/quickshell/Modules/Settings/LockScreenTab.qml +++ b/quickshell/Modules/Settings/LockScreenTab.qml @@ -156,6 +156,57 @@ Item { } } + SettingsCard { + width: parent.width + iconName: "palette" + title: I18n.tr("Lock Screen Appearance") + settingKey: "lockAppearance" + + StyledText { + text: I18n.tr("Customize the font and background of the lock screen, or leave empty to use your theme font and desktop wallpaper. Changes apply instantly.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsFontDropdownRow { + settingKey: "lockScreenFontFamily" + tags: ["lock", "screen", "font", "typography"] + text: I18n.tr("Lock screen font") + description: I18n.tr("Font used for the clock and date on the lock screen") + currentFont: SettingsData.lockScreenFontFamily || "" + onFontSelected: family => SettingsData.set("lockScreenFontFamily", family) + } + + StyledText { + text: I18n.tr("Background") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + } + + StyledText { + text: I18n.tr("Use a custom image for the lock screen, or leave empty to use your desktop wallpaper.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.Wrap + } + + SettingsWallpaperPicker { + width: parent.width + path: SettingsData.lockScreenWallpaperPath + fillMode: SettingsData.lockScreenWallpaperFillMode + browserTitle: I18n.tr("Select lock screen background image") + fillModeSettingKey: "lockScreenWallpaperFillMode" + fillModeTags: ["lock", "screen", "wallpaper", "background", "fill"] + onPathSelected: path => SettingsData.set("lockScreenWallpaperPath", path) + onFillModeSelected: mode => SettingsData.set("lockScreenWallpaperFillMode", mode) + } + } + SettingsCard { width: parent.width iconName: "lock" diff --git a/quickshell/Modules/Settings/Widgets/SettingsFontDropdownRow.qml b/quickshell/Modules/Settings/Widgets/SettingsFontDropdownRow.qml new file mode 100644 index 000000000..12d80cb78 --- /dev/null +++ b/quickshell/Modules/Settings/Widgets/SettingsFontDropdownRow.qml @@ -0,0 +1,33 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common + +SettingsDropdownRow { + id: root + + property string currentFont: "" + signal fontSelected(string family) + + property var _families: ["Default"] + property bool _enumerated: false + + function _enumerate() { + if (_enumerated) + return; + var fonts = Qt.fontFamilies().filter(f => !f.startsWith(".")); + fonts.sort(); + fonts.unshift("Default"); + _families = fonts; + _enumerated = true; + } + + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 400 + options: _families + currentValue: (root.currentFont === "" || root.currentFont === Theme.defaultFontFamily) ? "Default" : root.currentFont + onValueChanged: value => root.fontSelected(value === "Default" ? "" : value) + + Component.onCompleted: Qt.callLater(_enumerate) +} diff --git a/quickshell/Modules/Settings/Widgets/SettingsWallpaperPicker.qml b/quickshell/Modules/Settings/Widgets/SettingsWallpaperPicker.qml new file mode 100644 index 000000000..31c73af61 --- /dev/null +++ b/quickshell/Modules/Settings/Widgets/SettingsWallpaperPicker.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Modals.FileBrowser +import qs.Widgets + +Column { + id: root + + property string path: "" + property string fillMode: "" + property string fallbackFillMode: "Fill" + property string browserTitle: I18n.tr("Select background image") + property string placeholderText: I18n.tr("Use desktop wallpaper") + property string fillModeSettingKey: "" + property var fillModeTags: [] + + signal pathSelected(string path) + signal fillModeSelected(string mode) + + readonly property var _fillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"] + + spacing: Theme.spacingS + + FileBrowserModal { + id: wallpaperBrowserModal + browserTitle: root.browserTitle + browserIcon: "wallpaper" + browserType: "wallpaper" + showHiddenFiles: true + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"] + onFileSelected: path => { + root.pathSelected(path); + close(); + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + DankTextField { + id: wallpaperPathField + width: parent.width - browseWallpaperButton.width - Theme.spacingS + placeholderText: root.placeholderText + text: root.path + backgroundColor: Theme.surfaceContainerHighest + onTextChanged: { + if (text !== root.path) + root.pathSelected(text); + } + } + + DankButton { + id: browseWallpaperButton + text: I18n.tr("Browse") + horizontalPadding: Theme.spacingL + onClicked: wallpaperBrowserModal.open() + } + } + + SettingsDropdownRow { + settingKey: root.fillModeSettingKey + tags: root.fillModeTags + text: I18n.tr("Wallpaper fill mode") + description: I18n.tr("How the background image is scaled") + options: root._fillModes.map(m => I18n.tr(m, "wallpaper fill mode")) + currentValue: { + var mode = (root.fillMode && root.fillMode !== "") ? root.fillMode : root.fallbackFillMode; + var idx = root._fillModes.indexOf(mode); + return idx >= 0 ? I18n.tr(root._fillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode"); + } + onValueChanged: value => { + var idx = root._fillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value); + if (idx >= 0) + root.fillModeSelected(root._fillModes[idx]); + } + } +} diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 52b00f5f0..ebda7a0d7 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -4688,6 +4688,29 @@ ], "icon": "lock" }, + { + "section": "lockAppearance", + "label": "Lock Screen Appearance", + "tabIndex": 11, + "category": "Lock Screen", + "keywords": [ + "appearance", + "clock", + "date", + "font", + "lock", + "lockscreen", + "login", + "password", + "screen", + "security", + "time", + "typography", + "watch" + ], + "icon": "palette", + "description": "Font used for the clock and date on the lock screen" + }, { "section": "lockDisplay", "label": "Lock Screen Display", @@ -7823,22 +7846,6 @@ ], "icon": "info" }, - { - "section": "greeterFontFamily", - "label": "Greeter font", - "tabIndex": 31, - "category": "Greeter", - "keywords": [ - "display manager", - "font", - "greetd", - "greeter", - "login", - "screen", - "typography" - ], - "description": "Font used on the login screen" - }, { "section": "greeterAuth", "label": "Login Authentication", @@ -7925,28 +7932,6 @@ "time" ] }, - { - "section": "greeterWallpaperFillMode", - "label": "Wallpaper fill mode", - "tabIndex": 31, - "category": "Greeter", - "keywords": [ - "background", - "bg", - "desktop", - "display manager", - "fill", - "greetd", - "greeter", - "image", - "login", - "mode", - "picture", - "scaled", - "wallpaper" - ], - "description": "How the background image is scaled" - }, { "section": "muxType", "label": "Multiplexer",