aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 13:26:07 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 13:26:07 +0200
commit96841b1cef108130bcf1ab21c34cf89b92f1b35c (patch)
tree29ecfe725930f6e27e72574d7a8d43172370da7b
parent8f749d8b1fe5c2740d8829a78fbfbb3c739dade9 (diff)
downloadsddm-theme-udt-96841b1cef108130bcf1ab21c34cf89b92f1b35c.tar.gz
sddm-theme-udt-96841b1cef108130bcf1ab21c34cf89b92f1b35c.zip
fix: pin the login card per SDDM screen window
-rw-r--r--README.md4
-rw-r--r--theme/Main.qml118
-rw-r--r--theme/components/LoginCard.qml9
-rw-r--r--theme/theme.conf4
4 files changed, 71 insertions, 64 deletions
diff --git a/README.md b/README.md
index 3c3df94..e043918 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,10 @@ its colours from the UDT palette, including the wallpaper-snapped accent.
SDDM merges into the theme config at every greeter start. Wallpaper and accent
need no root re-run; only theme code changes do.
+Set `uiScreen` in the root-owned `theme/theme.conf` to pin the login card to a
+named screen; leave it empty for the primary screen. It does not belong in the
+generated `theme.conf.user`, which is overwritten on every wallpaper change.
+
## Install
The theme code is copied by root. UDT's `install.sh` prints the block; run it in
diff --git a/theme/Main.qml b/theme/Main.qml
index e5c529f..a813d6c 100644
--- a/theme/Main.qml
+++ b/theme/Main.qml
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
-// UDT SDDM greeter. The root item spans every screen. The wallpaper is drawn
-// once per screen; the login card is pinned to one screen (config uiScreen,
-// else the primary screen); input wakes the card after it fades.
+// UDT SDDM greeter. SDDM creates one window per screen, each sized to its own
+// screen and carrying a one-row ScreenModel, so this root item is exactly one
+// screen. The wallpaper fills it; the login card draws only on the screen
+// named by config uiScreen, else on the primary screen. Input wakes the card
+// after it fades.
import QtQuick
import "components"
@@ -11,81 +13,68 @@ import "components"
Item {
id: root
- // SDDM sizes the greeter to the union of every screen.
- readonly property var union: screenModel.geometry(-1)
- width: union.width
- height: union.height
+ // SDDM creates one window per screen and sizes it to that screen. The
+ // model holds exactly one row and geometry(index) ignores index, returning
+ // this window's own screen at origin (0,0).
+ readonly property var windowGeometry: screenModel.geometry(0)
+ width: windowGeometry.width
+ height: windowGeometry.height
// Palette. Values come from theme.conf, overridden live by theme.conf.user.
readonly property string baseColor: config.stringValue("base")
- readonly property int cardRadius: 10
readonly property string wallpaperUrl:
config.stringValue("background") === ""
? ""
: "file://" + config.stringValue("background")
+ // The screen the card belongs on: config uiScreen if non-empty, else the
+ // primary screen. `primaryScreen` is a per-window context property SDDM
+ // sets true only on the window showing the primary screen.
readonly property string wantedScreen: config.stringValue("uiScreen")
- property var primaryGeometry: screenModel.geometry(screenModel.primary >= 0 ? screenModel.primary : 0)
- property var namedGeometry: primaryGeometry
- readonly property var cardGeometry: wantedScreen === "" ? primaryGeometry : namedGeometry
+ property string thisScreenName: ""
+ readonly property bool cardScreen: wantedScreen === ""
+ ? primaryScreen
+ : (thisScreenName === wantedScreen)
+
+ // The wallpaper fills this window's single screen. The frosted card
+ // samples this image.
+ Image {
+ id: wallpaperImage
+ anchors.fill: parent
+ source: root.wallpaperUrl
+ fillMode: Image.PreserveAspectCrop
+ sourceSize.width: width
+ sourceSize.height: height
+ smooth: true
+ asynchronous: true
+ }
- // The Image delegate of the screen the card sits on. The frosted card
- // samples it. Null until the model has populated.
- property Item cardScreenImage: null
+ // Scrim so the card and text read on any wallpaper.
+ Rectangle {
+ anchors.fill: parent
+ color: root.baseColor
+ opacity: 0.45
+ }
+ // Non-visual: reads the model's single row to learn this screen's name.
Repeater {
- id: screens
model: screenModel
delegate: Item {
- x: geometry.x
- y: geometry.y
- width: geometry.width
- height: geometry.height
-
- Image {
- id: screenImage
- anchors.fill: parent
- source: root.wallpaperUrl
- fillMode: Image.PreserveAspectCrop
- sourceSize.width: parent.width
- sourceSize.height: parent.height
- smooth: true
- asynchronous: true
- }
-
- // Scrim so the card and text read on any wallpaper.
- Rectangle {
- anchors.fill: parent
- color: root.baseColor
- opacity: 0.45
- }
-
- readonly property string screenName: name
-
- Component.onCompleted: root.registerScreen(index, screenName, screenImage, geometry)
+ visible: false
+ Component.onCompleted: root.thisScreenName = name
}
}
- // Filled as the model populates: the primary screen's name, and the
- // wallpaper image plus geometry of whichever screen owns the card.
- property string primaryName: ""
-
- function registerScreen(index, screenName, image, geom) {
- if (index === screenModel.primary)
- root.primaryName = screenName
- if (screenName === root.cardScreenName()) {
- root.cardScreenImage = image
- root.namedGeometry = geom
- }
- }
-
- function cardScreenName() {
- return root.wantedScreen !== "" ? root.wantedScreen : root.primaryName
- }
-
// Idle: fade the card after fadeoutMs, restore on any input.
property bool uiVisible: true
+ // While faded the card is disabled, which clears its focus; take focus on
+ // the root so the first key press wakes the card instead of vanishing.
+ onUiVisibleChanged: {
+ if (!uiVisible)
+ root.forceActiveFocus()
+ }
+
Timer {
id: idle
interval: config.intValue("fadeoutMs") > 0 ? config.intValue("fadeoutMs") : 60000
@@ -95,7 +84,10 @@ Item {
}
function wake() {
- root.uiVisible = true
+ if (!root.uiVisible) {
+ root.uiVisible = true
+ card.restoreFocus()
+ }
idle.restart()
}
@@ -113,12 +105,12 @@ Item {
LoginCard {
id: card
- x: root.cardGeometry.x + (root.cardGeometry.width - width) / 2 + card.shakeOffset
- y: root.cardGeometry.y + (root.cardGeometry.height - height) / 2
- backdropSource: root.cardScreenImage
- visible: opacity > 0
+ anchors.centerIn: parent
+ visible: root.cardScreen
+ enabled: root.uiVisible
opacity: root.uiVisible ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 260; easing.type: Easing.OutCubic } }
+ backdropSource: wallpaperImage
onWake: root.wake()
}
}
diff --git a/theme/components/LoginCard.qml b/theme/components/LoginCard.qml
index 5f336d0..d76269f 100644
--- a/theme/components/LoginCard.qml
+++ b/theme/components/LoginCard.qml
@@ -239,6 +239,15 @@ Item {
sddm.login(usernameField.text, passwordField.text, sessionBox.currentIndex)
}
+ // Return keyboard focus after the idle fade disabled the card: password
+ // when a username is already filled, else the username field.
+ function restoreFocus() {
+ if (usernameField.text.length > 0)
+ passwordField.forceActiveFocus()
+ else
+ usernameField.forceActiveFocus()
+ }
+
// Login failure wins the notice line; otherwise Caps Lock, if on.
property string loginError: ""
readonly property string message: {
diff --git a/theme/theme.conf b/theme/theme.conf
index c2b8269..b5af812 100644
--- a/theme/theme.conf
+++ b/theme/theme.conf
@@ -26,7 +26,9 @@ blue=#8aadf4
lavender=#b7bdf8
accent=#b7bdf8
-# Empty uiScreen pins the card to the primary screen.
+# Empty uiScreen pins the card to the primary screen. Set a screen name here,
+# in this root-owned file: theme.conf.user is regenerated on every wallpaper
+# change and is not the place for uiScreen.
uiScreen=
# 60000 matches the stock theme's idle timeout.
fadeoutMs=60000