Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Symlinks created by preview/lint scripts at runtime
preview/components
preview/assets
preview/.reload-signal
169 changes: 34 additions & 135 deletions preview/Preview.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,13 @@ import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls

import "components"

/*
* Preview.qml - Development harness for the SDDM theme.
*
* Mirrors the Main.qml layout but provides mock objects in place of
* the SDDM context properties (sddm, userModel, sessionModel, config,
* screenModel, keyboard).
*
* Components receive their dependencies as explicit properties, so they
* work identically whether driven by SDDM or by this preview.
*
* The preview script symlinks components/ and assets/ into preview/
* so that relative imports and asset paths resolve to the selected theme.
* Provides mock objects in place of the SDDM context properties, then loads
* the theme layout through a Loader. A file-watcher timer detects changes
* (via a signal file touched by entr) and reloads the Loader in-place,
* keeping the window open for a true live-preview experience.
*
* Usage: ./scripts/preview.sh [-theme <name>]
*/
Expand All @@ -32,10 +25,7 @@ Window {
// Mock objects
// ═══════════════════════════════════════════════════════════════

QtObject {
id: mockConfig
// Set to "" to use the solid fallback color, or provide a path
// to an image (e.g. drop your own into assets/background.jpg).
property QtObject mockConfig: QtObject {
property string background: Qt.resolvedUrl("assets/background.jpg")
property string type: "image"
property string color: "#1a1a2e"
Expand All @@ -52,8 +42,7 @@ Window {
property int screenHeight: previewWindow.height
}

QtObject {
id: mockSddm
property QtObject mockSddm: QtObject {
property string hostname: "preview-host"
property bool canPowerOff: true
property bool canReboot: true
Expand Down Expand Up @@ -81,8 +70,7 @@ Window {
function hybridSleep() { console.log("[mock] sddm.hybridSleep()") }
}

ListModel {
id: mockUserModel
property ListModel mockUserModel: ListModel {
property string lastUser: "user"
property int lastIndex: 0
property int disableAvatarsThreshold: 7
Expand All @@ -92,17 +80,15 @@ Window {
ListElement { name: "guest"; realName: "Guest User"; icon: ""; needsPassword: true }
}

ListModel {
id: mockSessionModel
property ListModel mockSessionModel: ListModel {
property int lastIndex: 0

ListElement { name: "Plasma (Wayland)"; comment: "" }
ListElement { name: "Plasma (X11)"; comment: "" }
ListElement { name: "GNOME"; comment: "" }
}

QtObject {
id: mockKeyboard
property QtObject mockKeyboard: QtObject {
property bool capsLock: false
property bool numLock: true
}
Expand All @@ -118,130 +104,43 @@ Window {
property color accentColor: mockConfig.accentColor || "#4a9eff"

// ═══════════════════════════════════════════════════════════════
// Theme layout (identical to Main.qml)
// Hot-reload Loader
// ═══════════════════════════════════════════════════════════════

// ── Background ───────────────────────────────────────────────
Image {
id: backgroundImage
// Hot-reload: `hotReloader` is a context property injected by
// preview-host.py. Reload is a 3-phase process orchestrated by Python:
// 1. unloadRequested β†’ QML destroys all component instances
// 2. Python clears the now-unreferenced component cache
// 3. reloadRequested β†’ QML reloads components from disk
//
// When hotReloader is not present (e.g. launched via plain qml6),
// the theme loads once without hot-reload.

Loader {
id: themeLoader
anchors.fill: parent
source: mockConfig.background || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
onStatusChanged: {
if (status === Image.Error && source != "")
console.log("Background image not found β€” using fallback color. " +
"Drop an image into assets/background.jpg or update theme.conf.")
}
}

Rectangle {
id: backgroundFallback
anchors.fill: parent
color: mockConfig.color || "#1a1a2e"
visible: backgroundImage.status !== Image.Ready
}

Rectangle {
anchors.fill: parent
color: mockConfig.backgroundOverlayColor || "#000000"
opacity: mockConfig.backgroundOverlayOpacity || 0.3
}

// ── Clock ────────────────────────────────────────────────────
Clock {
id: clock
visible: mockConfig.clockVisible === "true"
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: previewWindow.height * 0.08
onStatusChanged: {
if (status === Loader.Error)
console.log("[hot-reload] Error loading ThemeLayout.qml β€” check for syntax errors")
}
textColor: primaryColor
fontSize: baseFontSize * 4
timeFormat: mockConfig.clockFormat || "hh:mm"
dateFormat: mockConfig.dateFormat || "dddd, MMMM d"
}

// ── Login form ───────────────────────────────────────────────
LoginForm {
id: loginForm
anchors.centerIn: parent
width: 320

textColor: primaryColor
accentColor: previewWindow.accentColor
fontSize: baseFontSize
fontFamily: previewWindow.fontFamily
defaultUsername: mockUserModel.lastUser || ""
notificationMessage: previewWindow.notificationMessage
capsLockOn: mockKeyboard.capsLock

sessionIndex: sessionSelector.currentIndex

onLoginRequest: function(username, password) {
previewWindow.notificationMessage = ""
mockSddm.login(username, password, sessionSelector.currentIndex)
}
function loadTheme() {
themeLoader.setSource("ThemeLayout.qml", { "preview": previewWindow })
}

// ── Footer ───────────────────────────────────────────────────
RowLayout {
id: footer
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
margins: 16
}
height: 48

SessionSelector {
id: sessionSelector
textColor: primaryColor
fontSize: baseFontSize - 2
fontFamily: previewWindow.fontFamily
sessions: mockSessionModel
Layout.preferredWidth: 180
Layout.preferredHeight: 32
}

Item { Layout.fillWidth: true }

PowerBar {
id: powerBar
textColor: primaryColor
fontSize: baseFontSize - 2
iconSize: baseFontSize + 6
Layout.preferredHeight: 48

canSuspend: mockSddm.canSuspend
canReboot: mockSddm.canReboot
canPowerOff: mockSddm.canPowerOff

onSuspendClicked: mockSddm.suspend()
onRebootClicked: mockSddm.reboot()
onPowerOffClicked: mockSddm.powerOff()
}
function unloadTheme() {
themeLoader.source = ""
}

// ── SDDM connections ─────────────────────────────────────────
Connections {
target: mockSddm
function onLoginFailed() {
previewWindow.notificationMessage = "Login Failed"
loginForm.clearPassword()
}
function onLoginSucceeded() {
previewWindow.notificationMessage = ""
}
target: typeof hotReloader !== "undefined" ? hotReloader : null
function onUnloadRequested() { previewWindow.unloadTheme() }
function onReloadRequested() { previewWindow.loadTheme() }
}

Timer {
interval: 3000
running: notificationMessage !== ""
onTriggered: notificationMessage = ""
}
Component.onCompleted: loadTheme()

// ── Preview overlay ──────────────────────────────────────────
Rectangle {
Expand All @@ -266,8 +165,8 @@ Window {
}
Text { text: "|"; color: "#80ffffff"; font.pointSize: 9 }
Text {
text: "Preview Mode"
color: "#ffcc00"
text: "Live Preview"
color: "#00ff88"
font.pointSize: 9
font.bold: true
}
Expand Down
148 changes: 148 additions & 0 deletions preview/ThemeLayout.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

import "components"

/*
* ThemeLayout.qml - Theme content loaded via Loader for hot reload.
*
* Accesses mock objects and derived properties from the parent Window
* through the `preview` property, which must be set by the Loader.
*/

Item {
id: layout
anchors.fill: parent

required property var preview

// Convenience aliases
property color primaryColor: preview.primaryColor
property color accentColor: preview.accentColor
property int baseFontSize: preview.baseFontSize
property string fontFamily: preview.fontFamily

// ── Background ───────────────────────────────────────────────
Image {
id: backgroundImage
anchors.fill: parent
source: preview.mockConfig.background || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
onStatusChanged: {
if (status === Image.Error && source != "")
console.log("Background image not found β€” using fallback color. " +
"Drop an image into assets/background.jpg or update theme.conf.")
}
}

Rectangle {
id: backgroundFallback
anchors.fill: parent
color: preview.mockConfig.color || "#1a1a2e"
visible: backgroundImage.status !== Image.Ready
}

Rectangle {
anchors.fill: parent
color: preview.mockConfig.backgroundOverlayColor || "#000000"
opacity: preview.mockConfig.backgroundOverlayOpacity || 0.3
}

// ── Clock ────────────────────────────────────────────────────
Clock {
id: clock
visible: preview.mockConfig.clockVisible === "true"
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: preview.height * 0.08
}
textColor: primaryColor
fontSize: baseFontSize * 4
timeFormat: preview.mockConfig.clockFormat || "hh:mm"
dateFormat: preview.mockConfig.dateFormat || "dddd, MMMM d"
}

// ── Login form ───────────────────────────────────────────────
LoginForm {
id: loginForm
anchors.centerIn: parent
width: 320

textColor: primaryColor
accentColor: layout.accentColor
fontSize: baseFontSize
fontFamily: layout.fontFamily
defaultUsername: preview.mockUserModel.lastUser || ""
notificationMessage: preview.notificationMessage
capsLockOn: preview.mockKeyboard.capsLock

sessionIndex: sessionSelector.currentIndex

onLoginRequest: function(username, password) {
preview.notificationMessage = ""
preview.mockSddm.login(username, password, sessionSelector.currentIndex)
}
}

// ── Footer ───────────────────────────────────────────────────
RowLayout {
id: footer
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
margins: 16
}
height: 48

SessionSelector {
id: sessionSelector
textColor: primaryColor
fontSize: baseFontSize - 2
fontFamily: layout.fontFamily
sessions: preview.mockSessionModel
Layout.preferredWidth: 180
Layout.preferredHeight: 32
}

Item { Layout.fillWidth: true }

PowerBar {
id: powerBar
textColor: primaryColor
fontSize: baseFontSize - 2
iconSize: baseFontSize + 6
Layout.preferredHeight: 48

canSuspend: preview.mockSddm.canSuspend
canReboot: preview.mockSddm.canReboot
canPowerOff: preview.mockSddm.canPowerOff

onSuspendClicked: preview.mockSddm.suspend()
onRebootClicked: preview.mockSddm.reboot()
onPowerOffClicked: preview.mockSddm.powerOff()
}
}

// ── SDDM connections ─────────────────────────────────────────
Connections {
target: preview.mockSddm
function onLoginFailed() {
preview.notificationMessage = "Login Failed"
loginForm.clearPassword()
}
function onLoginSucceeded() {
preview.notificationMessage = ""
}
}

Timer {
interval: 3000
running: preview.notificationMessage !== ""
onTriggered: preview.notificationMessage = ""
}
}
Loading