Skip to content

Commit 9c54180

Browse files
authored
Merge pull request #6 from derryl/theme-tweaks
Redesign login form (eos-breeze style) and eliminate preview/theme duplication
2 parents 2897182 + 62a6eb7 commit 9c54180

9 files changed

Lines changed: 765 additions & 382 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Symlinks created by preview/lint scripts at runtime
2+
preview/Main.qml
23
preview/components
34
preview/assets
45
preview/.reload-signal
6+
*/__pycache__

CLAUDE.md

Lines changed: 282 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,25 @@ A development environment for building and customizing SDDM login themes for KDE
99
## Commands
1010

1111
### Live preview (primary development loop)
12+
1213
```sh
1314
./scripts/preview.sh # preview the default theme
1415
./scripts/preview.sh -theme <name> # preview a specific theme
15-
qml6 preview/Preview.qml # one-shot preview (no file watching)
1616
```
1717

18+
The preview requires the Python host (`preview-host.py`) — there is no standalone `qml6` launch path. The host injects mock SDDM context properties and provides hot-reload via file watching.
19+
1820
### Lint (static analysis + runtime type check)
21+
1922
```sh
2023
./scripts/lint-qml.sh # lint all themes
2124
./scripts/lint-qml.sh -theme <name> # lint a specific theme
2225
```
23-
CI runs this on every push/PR. It runs `qmllint` for static analysis, then launches the preview under `xvfb` for 3 seconds to catch runtime type errors (`Unable to assign`, `ReferenceError`, `TypeError`).
26+
27+
CI runs this on every push/PR. It runs `qmllint` for static analysis, then launches the preview host under `xvfb` for 3 seconds to catch runtime type errors (`Unable to assign`, `ReferenceError`, `TypeError`).
2428

2529
### Full-fidelity SDDM test
30+
2631
```sh
2732
./scripts/test-sddm.sh # test default theme with real SDDM greeter
2833
./scripts/test-sddm.sh -theme <name>
@@ -31,29 +36,294 @@ CI runs this on every push/PR. It runs `qmllint` for static analysis, then launc
3136
## Architecture
3237

3338
### Theme structure
39+
3440
Themes live under `themes/<name>/`. The default theme is at `themes/default/`.
3541

36-
Each theme contains:
37-
- `Main.qml` — SDDM entry point; root `Item` that uses SDDM-injected context properties
38-
- `theme.conf` — all configurable values (colors, fonts, background, clock format, screen dimensions)
39-
- `metadata.desktop` — SDDM theme registration metadata
40-
- `components/` — UI components (Clock, LoginForm, SessionSelector, PowerBar)
41-
- `assets/` — background image and other static assets
42+
Each theme is fully self-contained:
43+
44+
```
45+
themes/<name>/
46+
Main.qml # SDDM entry point; root Item using SDDM context properties
47+
theme.conf # all configurable values (colors, fonts, background, etc.)
48+
metadata.desktop # SDDM theme registration metadata
49+
components/ # UI components (Clock, LoginForm, SessionSelector, PowerBar)
50+
Clock.qml
51+
LoginForm.qml
52+
PowerBar.qml
53+
SessionSelector.qml
54+
assets/ # background image and other static assets
55+
background.jpg
56+
faces/ # user avatar images (optional)
57+
```
58+
59+
Any number of themes can coexist in `themes/` and be previewed interchangeably.
4260

4361
### Preview harness
44-
`preview/Preview.qml` mirrors `Main.qml` layout but replaces SDDM context properties with mock objects (`mockSddm`, `mockConfig`, `mockUserModel`, `mockSessionModel`, `mockKeyboard`). The preview script symlinks the selected theme's `components/` and `assets/` into `preview/` so relative QML imports resolve correctly.
62+
63+
The preview system is designed so themes need zero awareness of the preview environment. The real SDDM greeter and the preview host both inject the same five context properties, so `Main.qml` works identically in both.
64+
65+
**How it works:**
66+
67+
1. `preview.sh` symlinks the selected theme's `Main.qml`, `components/`, and `assets/` into `preview/` so QML relative imports resolve correctly.
68+
2. `preview-host.py` (PyQt6) creates mock objects for all five SDDM context properties and registers them via `engine.rootContext().setContextProperty()` — the same mechanism SDDM uses.
69+
3. `Preview.qml` is a thin window shell with a `Loader` that loads `Main.qml`. It also binds `config.screenWidth`/`config.screenHeight` to the window dimensions and provides the hot-reload overlay badge.
70+
4. The `HotReloader` watches theme files and triggers a 3-phase reload: unload Loader -> clear component cache -> reload Loader.
71+
72+
**Key design principle:** No theme-specific layout logic exists outside `themes/`. The `preview/` directory contains only the generic harness. Adding a new theme requires no changes to preview infrastructure.
4573

4674
In preview mode, use password `test` for successful login; any other password triggers "Login Failed".
4775

4876
### SDDM context properties
49-
SDDM injects five globals that themes depend on: `sddm`, `config`, `userModel`, `sessionModel`, `keyboard`. Components receive these as explicit properties rather than accessing globals directly, making them portable between Main.qml (real SDDM) and Preview.qml (mocks).
77+
78+
SDDM injects five globals that themes depend on:
79+
80+
| Property | Type | Description |
81+
|----------|------|-------------|
82+
| `sddm` | QObject | Proxy with `login()`, `powerOff()`, `reboot()`, `suspend()` and capability booleans (`canPowerOff`, `canReboot`, `canSuspend`) |
83+
| `config` | QObject | Key-value access to `theme.conf` `[General]` section (e.g., `config.primaryColor`, `config.fontPointSize`) |
84+
| `userModel` | QAbstractListModel | List of system users with roles: `name`, `realName`, `icon`, `needsPassword`. Also has `lastUser` property. |
85+
| `sessionModel` | QAbstractListModel | List of desktop sessions with roles: `name`, `comment`. Also has `lastIndex` property. |
86+
| `keyboard` | QObject | Keyboard state: `capsLock`, `numLock` booleans |
87+
88+
Components receive these as explicit properties rather than accessing globals directly, making them portable and testable.
89+
90+
### Config values
91+
92+
All config values from `theme.conf` are strings in QML. Main.qml parses them as needed:
93+
94+
```qml
95+
// Integer parsing with fallback
96+
property int baseFontSize: config.fontPointSize ? parseInt(config.fontPointSize) : 12
97+
98+
// Float parsing with fallback
99+
opacity: parseFloat(config.backgroundOverlayOpacity) || 0.3
100+
101+
// String comparison for booleans
102+
visible: config.clockVisible === "true"
103+
```
50104

51105
### QML style
52-
All scripts set `QT_QUICK_CONTROLS_STYLE=Basic` to avoid Breeze/Plasma-specific errors outside a full Plasma session.
106+
107+
All scripts set `QT_QUICK_CONTROLS_STYLE=Basic` to avoid Breeze/Plasma-specific errors outside a full Plasma session. Without this, ComboBox and other controls fail with `TypeError: Cannot read property 'Overlay' of undefined`.
108+
109+
## Key Files
110+
111+
### Theme files (per theme)
112+
113+
- `themes/<name>/Main.qml` — Root layout: background, clock, login form, power bar, footer. Wires SDDM context properties to component props.
114+
- `themes/<name>/theme.conf` — INI-format config under `[General]`. Keys: `background`, `primaryColor`, `accentColor`, `backgroundOverlayColor`, `backgroundOverlayOpacity`, `fontFamily`, `fontPointSize`, `clockVisible`, `clockFormat`, `dateFormat`, `screenWidth`, `screenHeight`.
115+
- `themes/<name>/components/LoginForm.qml` — Password field + login icon button in a horizontal Row. No username field; uses `defaultUsername` from `userModel.lastUser`.
116+
- `themes/<name>/components/PowerBar.qml` — Suspend/Reboot/Shut Down buttons using unicode icons. Inline `component PowerButton` definition.
117+
- `themes/<name>/components/Clock.qml` — Time + date display with configurable formats via Qt `timeFormat`/`dateFormat` strings.
118+
- `themes/<name>/components/SessionSelector.qml` — ComboBox for desktop session selection, bound to `sessionModel`.
119+
120+
### Preview infrastructure
121+
122+
- `preview/Preview.qml` — Thin window shell. Loader + hot-reload wiring + overlay badge. No mock objects or theme layout logic.
123+
- `scripts/preview-host.py` — PyQt6 host. Defines `MockSddm`, `MockConfig`, `MockUserModel`, `MockSessionModel`, `MockKeyboard`. Reads `theme.conf` for config values. Provides `HotReloader` for live preview.
124+
- `scripts/preview.sh` — Sets up symlinks (`Main.qml`, `components/`, `assets/` into `preview/`), launches `preview-host.py` with the theme directory as argument.
125+
- `scripts/lint-qml.sh` — Static + runtime lint. Uses `qmllint` then launches `preview-host.py` under `xvfb` to catch runtime type errors.
126+
127+
## QML / Qt Quick Guidelines
128+
129+
### Imports available
130+
131+
```qml
132+
import QtQuick // core types: Item, Rectangle, Text, Image, etc.
133+
import QtQuick.Layouts // RowLayout, ColumnLayout
134+
import QtQuick.Controls // TextField, Button, ComboBox
135+
import Qt5Compat.GraphicalEffects // DropShadow, InnerShadow, etc. (Qt 6 compat module)
136+
```
137+
138+
**Never import:** `org.kde.plasma.*`, `org.kde.breeze.components`, `Kirigami`, `PlasmaExtras`, or any KDE-specific modules. These break outside a full Plasma session.
139+
140+
### Applying graphical effects (DropShadow, etc.)
141+
142+
Use `layer.enabled` + `layer.effect` to apply effects inline without restructuring the component tree or hiding the source:
143+
144+
```qml
145+
TextField {
146+
layer.enabled: true
147+
layer.effect: DropShadow {
148+
horizontalOffset: 0
149+
verticalOffset: 2
150+
radius: 12.0
151+
samples: 25 // recommended: 1 + radius * 2
152+
color: "#40000000"
153+
transparentBorder: true
154+
}
155+
}
156+
```
157+
158+
The alternative (separate `DropShadow` item with `visible: false` on the source) works but requires restructuring sibling elements.
159+
160+
### Inset / recessed field effect
161+
162+
Simulate an inset border using two nested Rectangles inside a background `Item`:
163+
164+
```qml
165+
background: Item {
166+
Rectangle {
167+
anchors.fill: parent
168+
radius: 8
169+
color: "transparent"
170+
border.color: Qt.rgba(0, 0, 0, 0.5) // dark outer ring = shadow edge
171+
border.width: 1
172+
}
173+
Rectangle {
174+
anchors.fill: parent
175+
anchors.margins: 1
176+
radius: 7
177+
color: "#1e293b" // dark slate fill
178+
border.color: Qt.rgba(255, 255, 255, 0.06) // subtle light rim
179+
border.width: 1
180+
}
181+
}
182+
```
183+
184+
### Component.onCompleted
185+
186+
**Critical:** A QML component can only have ONE `Component.onCompleted` handler. If you define two, QML reports "Property value set multiple times" and the component silently fails to load. Merge all initialization into a single block:
187+
188+
```qml
189+
// WRONG — will fail silently
190+
Component.onCompleted: { doA() }
191+
Component.onCompleted: { doB() }
192+
193+
// CORRECT
194+
Component.onCompleted: {
195+
doA()
196+
doB()
197+
}
198+
```
199+
200+
### Color functions
201+
202+
```qml
203+
Qt.rgba(r, g, b, a) // 0.0–1.0 range
204+
Qt.darker(color, factor) // factor > 1.0 = darker
205+
Qt.lighter(color, factor) // factor > 1.0 = lighter
206+
"#AARRGGBB" // hex with alpha prefix
207+
```
208+
209+
### Focus management
210+
211+
```qml
212+
Component.onCompleted: {
213+
passwordField.forceActiveFocus()
214+
}
215+
216+
Keys.onReturnPressed: doLogin()
217+
Keys.onEnterPressed: doLogin() // numpad Enter — always handle both
218+
```
219+
220+
### Common QML patterns in this codebase
221+
222+
- `renderType: Text.NativeRendering` on all Text elements for crisp rendering at login screen resolution
223+
- `Behavior on color { ColorAnimation { duration: 120 } }` for smooth hover transitions
224+
- Components expose signals (e.g., `signal loginRequest(string username, string password)`) rather than calling SDDM directly
225+
- Inline component definitions via `component Name: Item { ... }` (used in PowerBar.qml)
226+
227+
## PyQt6 / Preview Host Guidelines
228+
229+
### Object lifetime (garbage collection)
230+
231+
When registering Python QObjects as QML context properties, keep Python references alive for the process lifetime. Without this, Python's GC frees the objects while QML still holds pointers, causing `null` access errors:
232+
233+
```python
234+
# WRONG — objects are GC'd immediately after setContextProperty returns
235+
ctx.setContextProperty("sddm", MockSddm())
236+
237+
# CORRECT — dict keeps references alive
238+
mocks = {
239+
"sddm": MockSddm(),
240+
"config": MockConfig(theme_dir),
241+
}
242+
for name, obj in mocks.items():
243+
ctx.setContextProperty(name, obj)
244+
```
245+
246+
### configparser key casing
247+
248+
Python's `configparser` lowercases all keys by default. `primaryColor` in theme.conf becomes `primarycolor`. Map explicitly:
249+
250+
```python
251+
@pyqtProperty(str, constant=True)
252+
def primaryColor(self): # QML accesses config.primaryColor
253+
return self._get("primarycolor") # configparser stored it lowercase
254+
```
255+
256+
### File paths for QML
257+
258+
QML `Image.source` expects either a relative path (resolved from the QML file's location) or a `file:///` URL. When providing paths from Python, always use absolute file URLs:
259+
260+
```python
261+
abs_bg = os.path.join(theme_dir, bg)
262+
self._values["background"] = QUrl.fromLocalFile(abs_bg).toString()
263+
# produces: file:///home/.../themes/default/assets/background.jpg
264+
```
265+
266+
**The `theme_dir` argument must be an absolute path** — use `os.path.abspath()` before any path joins.
267+
268+
### Writable properties from QML
269+
270+
To let QML write to a Python property (e.g., `config.screenWidth = width`), use the `fget`/`fset` form:
271+
272+
```python
273+
screenWidthChanged = pyqtSignal()
274+
275+
def _get_screen_width(self):
276+
return self._screen_width
277+
278+
def _set_screen_width(self, val):
279+
if self._screen_width != val:
280+
self._screen_width = val
281+
self.screenWidthChanged.emit()
282+
283+
screenWidth = pyqtProperty(int, fget=_get_screen_width, fset=_set_screen_width,
284+
notify=screenWidthChanged)
285+
```
286+
287+
### QAbstractListModel roles
288+
289+
SDDM models expose data via named roles. Python mocks must define `roleNames()` returning a dict of `{role_int: b"name"}` and handle those roles in `data()`:
290+
291+
```python
292+
_NameRole = Qt.ItemDataRole.UserRole + 1
293+
294+
def roleNames(self):
295+
return {self._NameRole: b"name"}
296+
297+
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
298+
if role == self._NameRole:
299+
return self._items[index.row()]["name"]
300+
```
53301

54302
## Key Conventions
55303

56-
- Pure QtQuick only — no `org.kde.plasma.*`, `org.kde.breeze.components`, or `Kirigami` imports
304+
- Pure QtQuick only — no KDE/Plasma imports
57305
- Components take dependencies as explicit props (not SDDM context property access)
58306
- `qmllint` warnings about unresolved SDDM globals are expected and tolerated; only actual errors fail CI
59307
- All config values come through `theme.conf` and are accessed via `config.<key>` in QML
308+
- Themes are fully self-contained — no files outside `themes/<name>/` should reference theme-specific layout
309+
- Always handle both `Keys.onReturnPressed` and `Keys.onEnterPressed` (keyboard Enter vs numpad Enter)
310+
- Always set `QT_QUICK_CONTROLS_STYLE=Basic` in any script that runs QML outside a Plasma session
311+
312+
## Documentation References (Context7)
313+
314+
Use Context7 to look up Qt/QML documentation. Key library IDs:
315+
316+
- **Qt 6 (full docs):** `/websites/doc_qt_io_qt-6_8` — covers all Qt modules, QML types, properties, and examples
317+
- DropShadow, InnerShadow: query `Qt5Compat.GraphicalEffects DropShadow`
318+
- Item layer effects: query `QML layer.enabled layer.effect`
319+
- TextField, ComboBox, Button: query `QtQuick.Controls <type>`
320+
- Image, Rectangle, Text: query `QtQuick <type> properties`
321+
- Loader: query `QML Loader setSource`
322+
- Property types and bindings: query `QML property binding`
323+
- Animations: query `QML Behavior ColorAnimation NumberAnimation`
324+
325+
### Useful external references
326+
327+
- SDDM theme API: the SDDM wiki on GitHub documents what context properties are injected and what roles the models expose. Search for `sddm theme api` or `sddm theme development`.
328+
- Qt Quick Controls styling: the `Basic` style is Qt's unstyled fallback. Search `Qt Quick Controls styles Basic`.
329+
- `eos-breeze-sddm` (GitHub: `endeavouros-team/eos-breeze-sddm`): reference SDDM theme this project draws design inspiration from.

0 commit comments

Comments
 (0)